mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-09-04 17:23:25 +00:00
ENH - overhaul Photos media experience
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 19 KiB |
@@ -6,6 +6,7 @@ import type { PhoneMedia } from '@/types/media'
|
|||||||
|
|
||||||
const photo: PhoneMedia = {
|
const photo: PhoneMedia = {
|
||||||
createdAt: 1_786_035_600,
|
createdAt: 1_786_035_600,
|
||||||
|
favorite: false,
|
||||||
id: 17,
|
id: 17,
|
||||||
mediaType: 'photo',
|
mediaType: 'photo',
|
||||||
url: 'https://media.example/photo.jpg',
|
url: 'https://media.example/photo.jpg',
|
||||||
|
|||||||
@@ -362,6 +362,7 @@ const defaultLocales: LocaleTree = {
|
|||||||
document: 'Document',
|
document: 'Document',
|
||||||
link: 'Link',
|
link: 'Link',
|
||||||
location: 'Location',
|
location: 'Location',
|
||||||
|
media: 'Media',
|
||||||
note: 'Note',
|
note: 'Note',
|
||||||
photo: 'Photo',
|
photo: 'Photo',
|
||||||
playlist: 'Playlist',
|
playlist: 'Playlist',
|
||||||
@@ -489,7 +490,7 @@ const defaultLocales: LocaleTree = {
|
|||||||
category: 'Category',
|
category: 'Category',
|
||||||
status: 'Publication Status',
|
status: 'Publication Status',
|
||||||
cover: 'Cover Image',
|
cover: 'Cover Image',
|
||||||
chooseCover: 'Choose from Gallery',
|
chooseCover: 'Choose from Photos',
|
||||||
changeCover: 'Change Cover',
|
changeCover: 'Change Cover',
|
||||||
removeCover: 'Remove Cover',
|
removeCover: 'Remove Cover',
|
||||||
coverAlt: 'Selected article cover',
|
coverAlt: 'Selected article cover',
|
||||||
@@ -561,7 +562,7 @@ const defaultLocales: LocaleTree = {
|
|||||||
login: 'Log in',
|
login: 'Log in',
|
||||||
register: 'Register',
|
register: 'Register',
|
||||||
ifruitEmail: 'iFruit address',
|
ifruitEmail: 'iFruit address',
|
||||||
gallery: 'Gallery',
|
gallery: 'Photos',
|
||||||
camera: 'Camera',
|
camera: 'Camera',
|
||||||
backToLogin: 'Back to CrewLink login',
|
backToLogin: 'Back to CrewLink login',
|
||||||
authErrors: {
|
authErrors: {
|
||||||
@@ -783,9 +784,9 @@ const defaultLocales: LocaleTree = {
|
|||||||
photo: 'Profile photo',
|
photo: 'Profile photo',
|
||||||
profilePhotos: 'Profile photos',
|
profilePhotos: 'Profile photos',
|
||||||
profilePhotosBody:
|
profilePhotosBody:
|
||||||
'Choose up to six photos from Gallery. Your first photo is shown first.',
|
'Choose up to six photos from Photos. Your first photo is shown first.',
|
||||||
addPhotos: 'Add photos',
|
addPhotos: 'Add photos',
|
||||||
choosePhotos: 'Choose from Gallery',
|
choosePhotos: 'Choose from Photos',
|
||||||
primaryPhoto: 'Main',
|
primaryPhoto: 'Main',
|
||||||
removePhoto: 'Remove photo {number}',
|
removePhoto: 'Remove photo {number}',
|
||||||
yourName: 'Your name',
|
yourName: 'Your name',
|
||||||
@@ -904,7 +905,7 @@ const defaultLocales: LocaleTree = {
|
|||||||
errors: {
|
errors: {
|
||||||
invalid_profile: 'Check your name, age and profile text.',
|
invalid_profile: 'Check your name, age and profile text.',
|
||||||
invalid_profile_photos:
|
invalid_profile_photos:
|
||||||
'Choose up to six photos from your own Gallery.',
|
'Choose up to six photos from your own Photos library.',
|
||||||
request_failed: 'Flare could not save those changes. Try again.',
|
request_failed: 'Flare could not save those changes. Try again.',
|
||||||
invalid_target: 'This profile is no longer available.',
|
invalid_target: 'This profile is no longer available.',
|
||||||
invalid_choice: 'That swipe could not be saved.',
|
invalid_choice: 'That swipe could not be saved.',
|
||||||
@@ -985,8 +986,8 @@ const defaultLocales: LocaleTree = {
|
|||||||
story: 'Story',
|
story: 'Story',
|
||||||
choosePhotos: 'Choose photos',
|
choosePhotos: 'Choose photos',
|
||||||
choosePhoto: 'Choose photo',
|
choosePhoto: 'Choose photo',
|
||||||
choosePhotosHint: 'Select up to five photos from Gallery',
|
choosePhotosHint: 'Select up to five photos from Photos',
|
||||||
chooseStoryHint: 'Select one photo from Gallery',
|
chooseStoryHint: 'Select one photo from Photos',
|
||||||
selectedPhotos: '{count} selected',
|
selectedPhotos: '{count} selected',
|
||||||
changePhotos: 'Change selection',
|
changePhotos: 'Change selection',
|
||||||
caption: 'Caption',
|
caption: 'Caption',
|
||||||
@@ -1231,7 +1232,7 @@ const defaultLocales: LocaleTree = {
|
|||||||
composerAudience: 'Everyone can join the conversation',
|
composerAudience: 'Everyone can join the conversation',
|
||||||
mediaTitle: 'Add to your feather',
|
mediaTitle: 'Add to your feather',
|
||||||
mediaBody: 'Share up to four photos from your phone or take a new one.',
|
mediaBody: 'Share up to four photos from your phone or take a new one.',
|
||||||
chooseGallery: 'Gallery',
|
chooseGallery: 'Photos',
|
||||||
chooseGalleryBody: 'Choose one or more saved photos',
|
chooseGalleryBody: 'Choose one or more saved photos',
|
||||||
takePhoto: 'Camera',
|
takePhoto: 'Camera',
|
||||||
takePhotoBody: 'Take a new photo now',
|
takePhotoBody: 'Take a new photo now',
|
||||||
@@ -1359,7 +1360,7 @@ const defaultLocales: LocaleTree = {
|
|||||||
createBody:
|
createBody:
|
||||||
'Choose a clip, set the cover and sound, then decide who can watch it.',
|
'Choose a clip, set the cover and sound, then decide who can watch it.',
|
||||||
chooseVideo: 'Choose a video',
|
chooseVideo: 'Choose a video',
|
||||||
chooseVideoHint: 'Select one from Gallery',
|
chooseVideoHint: 'Select one from Photos',
|
||||||
changeVideo: 'Change',
|
changeVideo: 'Change',
|
||||||
caption: 'Caption',
|
caption: 'Caption',
|
||||||
captionPlaceholder: 'Write a caption...',
|
captionPlaceholder: 'Write a caption...',
|
||||||
@@ -1435,7 +1436,7 @@ const defaultLocales: LocaleTree = {
|
|||||||
accountType: 'Account type',
|
accountType: 'Account type',
|
||||||
profilePhoto: 'Profile photo',
|
profilePhoto: 'Profile photo',
|
||||||
changePhoto: 'Change photo',
|
changePhoto: 'Change photo',
|
||||||
chooseFromGallery: 'Gallery',
|
chooseFromGallery: 'Photos',
|
||||||
takePhoto: 'Camera',
|
takePhoto: 'Camera',
|
||||||
removePhoto: 'Remove photo',
|
removePhoto: 'Remove photo',
|
||||||
noConnections: 'No profiles to show',
|
noConnections: 'No profiles to show',
|
||||||
@@ -1797,7 +1798,7 @@ const defaultLocales: LocaleTree = {
|
|||||||
phoneNumber: 'Phone Number',
|
phoneNumber: 'Phone Number',
|
||||||
officialContact: 'Official company contact',
|
officialContact: 'Official company contact',
|
||||||
choosePhoto: 'Choose Contact Photo',
|
choosePhoto: 'Choose Contact Photo',
|
||||||
chooseGallery: 'Gallery',
|
chooseGallery: 'Photos',
|
||||||
takePhoto: 'Camera',
|
takePhoto: 'Camera',
|
||||||
removePhoto: 'Remove Photo',
|
removePhoto: 'Remove Photo',
|
||||||
call: 'Call',
|
call: 'Call',
|
||||||
@@ -2348,7 +2349,7 @@ const defaultLocales: LocaleTree = {
|
|||||||
editProfile: 'Edit Profile',
|
editProfile: 'Edit Profile',
|
||||||
editProfileBody: 'Change your public name and profile photo',
|
editProfileBody: 'Change your public name and profile photo',
|
||||||
profilePhoto: 'Profile photo',
|
profilePhoto: 'Profile photo',
|
||||||
gallery: 'Gallery',
|
gallery: 'Photos',
|
||||||
camera: 'Camera',
|
camera: 'Camera',
|
||||||
profileDetails: 'Profile details',
|
profileDetails: 'Profile details',
|
||||||
profileName: 'Name',
|
profileName: 'Name',
|
||||||
@@ -2854,13 +2855,13 @@ const defaultLocales: LocaleTree = {
|
|||||||
publish: 'Publish',
|
publish: 'Publish',
|
||||||
addPhotos: 'Add photos',
|
addPhotos: 'Add photos',
|
||||||
addPhotosBody:
|
addPhotosBody:
|
||||||
'Choose up to six photos from your gallery or take new ones. Photos are optional and the first becomes the cover.',
|
'Choose up to six photos from Photos or take new ones. Photos are optional and the first becomes the cover.',
|
||||||
chooseGallery: 'Choose from gallery',
|
chooseGallery: 'Choose from Photos',
|
||||||
chooseGalleryBody: 'Use photos saved on this phone.',
|
chooseGalleryBody: 'Use photos saved on this phone.',
|
||||||
takePhotos: 'Take photos',
|
takePhotos: 'Take photos',
|
||||||
takePhotosBody: 'Take several new photos for this listing.',
|
takePhotosBody: 'Take several new photos for this listing.',
|
||||||
selectedPhotos: 'Selected photos',
|
selectedPhotos: 'Selected photos',
|
||||||
gallery: 'Gallery',
|
gallery: 'Photos',
|
||||||
camera: 'Camera',
|
camera: 'Camera',
|
||||||
takePhoto: 'Take photo',
|
takePhoto: 'Take photo',
|
||||||
cameraHint:
|
cameraHint:
|
||||||
@@ -3006,7 +3007,7 @@ const defaultLocales: LocaleTree = {
|
|||||||
profileBio: 'Bio',
|
profileBio: 'Bio',
|
||||||
profileBioPlaceholder: 'Tell the city a little about yourself...',
|
profileBioPlaceholder: 'Tell the city a little about yourself...',
|
||||||
profilePhoto: 'Profile photo',
|
profilePhoto: 'Profile photo',
|
||||||
profilePhotoHint: 'Choose a photo from your Gallery or take a new one.',
|
profilePhotoHint: 'Choose a photo from Photos or take a new one.',
|
||||||
removeProfilePhoto: 'Remove photo',
|
removeProfilePhoto: 'Remove photo',
|
||||||
profileSave: 'Save profile',
|
profileSave: 'Save profile',
|
||||||
profileCancel: 'Cancel',
|
profileCancel: 'Cancel',
|
||||||
@@ -3031,7 +3032,7 @@ const defaultLocales: LocaleTree = {
|
|||||||
photos: 'Photos',
|
photos: 'Photos',
|
||||||
optional: 'optional',
|
optional: 'optional',
|
||||||
camera: 'Camera',
|
camera: 'Camera',
|
||||||
gallery: 'Gallery',
|
gallery: 'Photos',
|
||||||
photoLimit: 'You can add up to six photos.',
|
photoLimit: 'You can add up to six photos.',
|
||||||
cityMarktShare: 'Share to Local Pages',
|
cityMarktShare: 'Share to Local Pages',
|
||||||
cityMarktShared: 'Shared to Local Pages.',
|
cityMarktShared: 'Shared to Local Pages.',
|
||||||
@@ -3176,11 +3177,11 @@ const defaultLocales: LocaleTree = {
|
|||||||
returnHelp: 'Space to return',
|
returnHelp: 'Space to return',
|
||||||
uploading: '{count} uploading',
|
uploading: '{count} uploading',
|
||||||
saving: 'Saving video...',
|
saving: 'Saving video...',
|
||||||
openGallery: 'Open Gallery',
|
openGallery: 'Open Photos',
|
||||||
takePhoto: 'Take photo',
|
takePhoto: 'Take photo',
|
||||||
startRecording: 'Start recording',
|
startRecording: 'Start recording',
|
||||||
stopRecording: 'Stop recording',
|
stopRecording: 'Stop recording',
|
||||||
saved: 'Saved to Gallery.',
|
saved: 'Saved to Photos.',
|
||||||
zoom: 'Set camera zoom to {zoom}',
|
zoom: 'Set camera zoom to {zoom}',
|
||||||
errors: {
|
errors: {
|
||||||
cancelled: 'Capture cancelled.',
|
cancelled: 'Capture cancelled.',
|
||||||
@@ -3607,15 +3608,50 @@ const defaultLocales: LocaleTree = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
photos: {
|
photos: {
|
||||||
name: 'Gallery',
|
name: 'Photos',
|
||||||
count: '{count} items',
|
library: 'Library',
|
||||||
loading: 'Loading Gallery...',
|
counts: {
|
||||||
|
all: '{count} Objects',
|
||||||
|
allOne: '{count} Object',
|
||||||
|
favorite: '{count} Favorite',
|
||||||
|
favorites: '{count} Favorites',
|
||||||
|
photo: '{count} Photo',
|
||||||
|
photos: '{count} Photos',
|
||||||
|
video: '{count} Video',
|
||||||
|
videos: '{count} Videos',
|
||||||
|
},
|
||||||
|
sorting: {
|
||||||
|
action: 'Sort Photos',
|
||||||
|
title: 'Sort Order',
|
||||||
|
show: 'Show',
|
||||||
|
allItems: 'All Items',
|
||||||
|
favorites: 'Favorites',
|
||||||
|
newestFirst: 'Newest First',
|
||||||
|
oldestFirst: 'Oldest First',
|
||||||
|
},
|
||||||
|
selection: {
|
||||||
|
action: 'Select',
|
||||||
|
selected: '{count} Selected',
|
||||||
|
share: 'Share Selected',
|
||||||
|
shareTitle: '{count} Media Items',
|
||||||
|
shareCopy: '{count} photos and videos shared from Photos.',
|
||||||
|
delete: 'Delete Selected',
|
||||||
|
deleteTitle: 'Delete Selected Media?',
|
||||||
|
deleteBody: 'The selected photos and videos will be permanently deleted.',
|
||||||
|
deleted: '{count} media items deleted.',
|
||||||
|
limit: 'You can select up to 50 media items.',
|
||||||
|
},
|
||||||
|
loading: 'Loading Photos...',
|
||||||
emptyTitle: 'No Photos or Videos',
|
emptyTitle: 'No Photos or Videos',
|
||||||
emptyBody: 'Captures from Camera will appear here.',
|
emptyBody: 'Captures from Camera will appear here.',
|
||||||
photo: 'Photo',
|
photo: 'Photo',
|
||||||
video: 'Video',
|
video: 'Video',
|
||||||
photoAlt: 'Gallery photo',
|
photoAlt: 'Photo library image',
|
||||||
videoAlt: 'Gallery video',
|
videoAlt: 'Photo library video',
|
||||||
|
today: 'Today',
|
||||||
|
yesterday: 'Yesterday',
|
||||||
|
addFavorite: 'Add to Favorites',
|
||||||
|
removeFavorite: 'Remove from Favorites',
|
||||||
delete: 'Delete media',
|
delete: 'Delete media',
|
||||||
deleteTitle: 'Delete Media?',
|
deleteTitle: 'Delete Media?',
|
||||||
deleteBody: 'This photo or video will be permanently deleted.',
|
deleteBody: 'This photo or video will be permanently deleted.',
|
||||||
@@ -3650,7 +3686,7 @@ const defaultLocales: LocaleTree = {
|
|||||||
invalid_media_type: 'The media type is invalid.',
|
invalid_media_type: 'The media type is invalid.',
|
||||||
invalid_upload: 'The upload could not be verified.',
|
invalid_upload: 'The upload could not be verified.',
|
||||||
invalid_upload_token: 'The upload session is no longer valid.',
|
invalid_upload_token: 'The upload session is no longer valid.',
|
||||||
missing_config: 'Gallery uploads are not configured.',
|
missing_config: 'Photos uploads are not configured.',
|
||||||
import_media_not_allowed: 'This media item is not allowed.',
|
import_media_not_allowed: 'This media item is not allowed.',
|
||||||
import_media_too_large: 'This media item is too large.',
|
import_media_too_large: 'This media item is too large.',
|
||||||
import_media_unavailable: 'This media item is no longer available.',
|
import_media_unavailable: 'This media item is no longer available.',
|
||||||
@@ -3667,7 +3703,7 @@ const defaultLocales: LocaleTree = {
|
|||||||
'Another media operation is already in progress.',
|
'Another media operation is already in progress.',
|
||||||
owner_changed: 'The active phone account changed.',
|
owner_changed: 'The active phone account changed.',
|
||||||
rate_limited: 'Too many media actions. Try again shortly.',
|
rate_limited: 'Too many media actions. Try again shortly.',
|
||||||
request_failed: 'The Gallery request failed.',
|
request_failed: 'The Photos request failed.',
|
||||||
request_timeout: 'The media service timed out.',
|
request_timeout: 'The media service timed out.',
|
||||||
unsupported: 'This media format is not supported.',
|
unsupported: 'This media format is not supported.',
|
||||||
upload_failed: 'The media upload failed.',
|
upload_failed: 'The media upload failed.',
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export type EasyShareKind =
|
|||||||
| 'document'
|
| 'document'
|
||||||
| 'link'
|
| 'link'
|
||||||
| 'location'
|
| 'location'
|
||||||
|
| 'media'
|
||||||
| 'note'
|
| 'note'
|
||||||
| 'photo'
|
| 'photo'
|
||||||
| 'playlist'
|
| 'playlist'
|
||||||
|
|||||||
@@ -3,11 +3,29 @@ export type GalleryFilter = 'all' | MediaType
|
|||||||
|
|
||||||
export type PhoneMedia = {
|
export type PhoneMedia = {
|
||||||
createdAt: number
|
createdAt: number
|
||||||
|
favorite: boolean
|
||||||
id: number
|
id: number
|
||||||
mediaType: MediaType
|
mediaType: MediaType
|
||||||
|
thumbnailUrl?: string
|
||||||
url: string
|
url: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type FavoriteResult = {
|
||||||
|
favorite: boolean
|
||||||
|
id: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GalleryCounts = {
|
||||||
|
all: number
|
||||||
|
favoritePhotos: number
|
||||||
|
favorites: number
|
||||||
|
favoriteVideos: number
|
||||||
|
photos: number
|
||||||
|
videos: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GallerySortOrder = 'newest' | 'oldest'
|
||||||
|
|
||||||
export type MediaImportSource = {
|
export type MediaImportSource = {
|
||||||
id: string
|
id: string
|
||||||
label: string
|
label: string
|
||||||
@@ -75,3 +93,10 @@ export type DeleteResult = {
|
|||||||
id?: number
|
id?: number
|
||||||
success: boolean
|
success: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DeleteManyResult = {
|
||||||
|
correlationId: string
|
||||||
|
deletedIds?: number[]
|
||||||
|
error?: string
|
||||||
|
success: boolean
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
bottomRightGridPosition,
|
||||||
filterMedia,
|
filterMedia,
|
||||||
formatMediaSize,
|
formatMediaSize,
|
||||||
formatRecordingDuration,
|
formatRecordingDuration,
|
||||||
hasNextMediaPage,
|
hasNextMediaPage,
|
||||||
mediaErrorKey,
|
mediaErrorKey,
|
||||||
mergeMedia,
|
mergeMedia,
|
||||||
|
orderMedia,
|
||||||
|
orderMediaOldestFirst,
|
||||||
} from './media'
|
} from './media'
|
||||||
|
|
||||||
const media = [
|
const media = [
|
||||||
{ createdAt: 10, id: 1, mediaType: 'photo' as const, url: 'photo' },
|
{ createdAt: 10, favorite: false, id: 1, mediaType: 'photo' as const, url: 'photo' },
|
||||||
{ createdAt: 20, id: 2, mediaType: 'video' as const, url: 'video' },
|
{ createdAt: 20, favorite: false, id: 2, mediaType: 'video' as const, url: 'video' },
|
||||||
]
|
]
|
||||||
|
|
||||||
describe('media utilities', () => {
|
describe('media utilities', () => {
|
||||||
@@ -24,8 +27,8 @@ describe('media utilities', () => {
|
|||||||
it('merges pages without duplicates and keeps newest first', () => {
|
it('merges pages without duplicates and keeps newest first', () => {
|
||||||
expect(
|
expect(
|
||||||
mergeMedia(media, [
|
mergeMedia(media, [
|
||||||
{ createdAt: 30, id: 1, mediaType: 'photo', url: 'updated' },
|
{ createdAt: 30, favorite: true, id: 1, mediaType: 'photo', url: 'updated' },
|
||||||
{ createdAt: 25, id: 3, mediaType: 'photo', url: 'new' },
|
{ createdAt: 25, favorite: false, id: 3, mediaType: 'photo', url: 'new' },
|
||||||
]).map((entry) => [entry.id, entry.url]),
|
]).map((entry) => [entry.id, entry.url]),
|
||||||
).toEqual([
|
).toEqual([
|
||||||
[1, 'updated'],
|
[1, 'updated'],
|
||||||
@@ -34,6 +37,28 @@ describe('media utilities', () => {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('orders the photo grid from oldest to newest', () => {
|
||||||
|
expect(orderMediaOldestFirst(media).map((entry) => entry.id)).toEqual([
|
||||||
|
1, 2,
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('orders the photo grid in the selected direction', () => {
|
||||||
|
expect(orderMedia(media, 'oldest').map((entry) => entry.id)).toEqual([1, 2])
|
||||||
|
expect(orderMedia(media, 'newest').map((entry) => entry.id)).toEqual([2, 1])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('fills the gallery from bottom-right to top-left', () => {
|
||||||
|
expect(bottomRightGridPosition(0, 1)).toEqual({ column: 3, row: 1 })
|
||||||
|
expect(bottomRightGridPosition(0, 3)).toEqual({ column: 3, row: 1 })
|
||||||
|
expect(bottomRightGridPosition(1, 3)).toEqual({ column: 2, row: 1 })
|
||||||
|
expect(bottomRightGridPosition(2, 3)).toEqual({ column: 1, row: 1 })
|
||||||
|
expect(bottomRightGridPosition(0, 4)).toEqual({ column: 3, row: 2 })
|
||||||
|
expect(bottomRightGridPosition(3, 4)).toEqual({ column: 3, row: 1 })
|
||||||
|
expect(bottomRightGridPosition(4, 5)).toEqual({ column: 2, row: 1 })
|
||||||
|
expect(bottomRightGridPosition(10, 11)).toEqual({ column: 2, row: 1 })
|
||||||
|
})
|
||||||
|
|
||||||
it('loads another gallery page only after a full 30-item batch', () => {
|
it('loads another gallery page only after a full 30-item batch', () => {
|
||||||
expect(hasNextMediaPage(30)).toBe(true)
|
expect(hasNextMediaPage(30)).toBe(true)
|
||||||
expect(hasNextMediaPage(29)).toBe(false)
|
expect(hasNextMediaPage(29)).toBe(false)
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import type { GalleryFilter, MediaType, PhoneMedia } from '@/types/media'
|
import type {
|
||||||
|
GalleryFilter,
|
||||||
|
GallerySortOrder,
|
||||||
|
MediaType,
|
||||||
|
PhoneMedia,
|
||||||
|
} from '@/types/media'
|
||||||
|
|
||||||
export const MEDIA_PAGE_SIZE = 30
|
export const MEDIA_PAGE_SIZE = 30
|
||||||
|
|
||||||
@@ -26,6 +31,37 @@ export function mergeMedia(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function orderMediaOldestFirst(media: PhoneMedia[]): PhoneMedia[] {
|
||||||
|
return [...media].sort(
|
||||||
|
(left, right) => left.createdAt - right.createdAt || left.id - right.id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function orderMedia(
|
||||||
|
media: PhoneMedia[],
|
||||||
|
sortOrder: GallerySortOrder,
|
||||||
|
): PhoneMedia[] {
|
||||||
|
return sortOrder === 'oldest'
|
||||||
|
? orderMediaOldestFirst(media)
|
||||||
|
: [...media].sort(
|
||||||
|
(left, right) =>
|
||||||
|
right.createdAt - left.createdAt || right.id - left.id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bottomRightGridPosition(
|
||||||
|
itemIndex: number,
|
||||||
|
itemCount: number,
|
||||||
|
columnCount = 3,
|
||||||
|
): { column: number; row: number } {
|
||||||
|
const count = Math.max(1, itemCount)
|
||||||
|
const index = Math.max(0, Math.min(itemIndex, count - 1))
|
||||||
|
return {
|
||||||
|
column: columnCount - (index % columnCount),
|
||||||
|
row: Math.ceil(count / columnCount) - Math.floor(index / columnCount),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function hasNextMediaPage(pageLength: number): boolean {
|
export function hasNextMediaPage(pageLength: number): boolean {
|
||||||
return pageLength === MEDIA_PAGE_SIZE
|
return pageLength === MEDIA_PAGE_SIZE
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { readFileSync } from 'node:fs'
|
||||||
|
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
|
const source = readFileSync(new URL('./GalleryApp.vue', import.meta.url), 'utf8')
|
||||||
|
const headerActions = source.slice(
|
||||||
|
source.indexOf('<template #right>'),
|
||||||
|
source.indexOf('</template>', source.indexOf('<template #right>')),
|
||||||
|
)
|
||||||
|
|
||||||
|
describe('GalleryApp import action', () => {
|
||||||
|
it('uses the photo viewer toolbar button design', () => {
|
||||||
|
expect(headerActions).toContain('<SkyToolbarPane')
|
||||||
|
expect(headerActions).toContain('gallery-header-tool--icon')
|
||||||
|
expect(headerActions).toContain('<SkyButton')
|
||||||
|
expect(headerActions).toContain('clear')
|
||||||
|
expect(headerActions).toContain(
|
||||||
|
'<Download :size="21" aria-hidden="true" />',
|
||||||
|
)
|
||||||
|
expect(headerActions).toContain(
|
||||||
|
':aria-label="phone.t(\'Apps.photos.import.action\')"',
|
||||||
|
)
|
||||||
|
expect(headerActions).toContain(
|
||||||
|
':title="phone.t(\'Apps.photos.import.action\')"',
|
||||||
|
)
|
||||||
|
expect(headerActions).not.toContain(
|
||||||
|
"{{ phone.t('Apps.photos.import.action') }}",
|
||||||
|
)
|
||||||
|
expect(headerActions).not.toContain('tonal')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders an iPhone-style large library header with filtered counts', () => {
|
||||||
|
expect(source).toContain('<SkyNavbar')
|
||||||
|
expect(source).toContain('variant="large"')
|
||||||
|
expect(source).toContain(':title="phone.t(\'Apps.photos.library\')"')
|
||||||
|
expect(source).toContain(':subtitle="countText"')
|
||||||
|
expect(source).toContain("nuiCall<GalleryCounts>('gallery:counts')")
|
||||||
|
expect(source).toContain("`Apps.photos.counts.${translationKey}`")
|
||||||
|
expect(source).toContain('gridColumnStart: bottomRightGridPosition(')
|
||||||
|
expect(source).toContain('gridRowStart: bottomRightGridPosition(')
|
||||||
|
expect(source).toContain('var(--sky-navbar-large-title-height) - 30px')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('opens an accessible sort menu from the large header', () => {
|
||||||
|
expect(headerActions).toContain('<ListFilter')
|
||||||
|
expect(headerActions).toContain('sortMenuOpened = true')
|
||||||
|
expect(source).toContain('<SkyActionSheet')
|
||||||
|
expect(source).toContain("selectSortOrder('newest')")
|
||||||
|
expect(source).toContain("selectSortOrder('oldest')")
|
||||||
|
expect(source).toContain('orderMedia(media.value, sortOrder.value)')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('supports selecting, sharing, and deleting multiple media items', () => {
|
||||||
|
expect(headerActions).toContain("phone.t('Apps.photos.selection.action')")
|
||||||
|
expect(headerActions).toContain('enterSelectionMode')
|
||||||
|
expect(source).toContain('v-if="selectionMode"')
|
||||||
|
expect(source).toContain('selectedCountText')
|
||||||
|
expect(source).toContain('shareSelection')
|
||||||
|
expect(source).toContain("kind: 'media'")
|
||||||
|
expect(source).toContain("nuiCall('gallery:delete-many'")
|
||||||
|
expect(source).toContain('media:deleteManyResult')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows capture time and iPhone-style actions in the media viewer', () => {
|
||||||
|
expect(source).toContain(':title="selectedCaptureDay"')
|
||||||
|
expect(source).toContain(':subtitle="selectedCaptureTime"')
|
||||||
|
expect(source).not.toContain("'Apps.photos.photo'\n : 'Apps.photos.video'")
|
||||||
|
expect(source).toContain("nuiCall<FavoriteResult>('gallery:favorite'")
|
||||||
|
expect(source).toContain('<Heart')
|
||||||
|
expect(source).toContain('gallery-detail-toolbar')
|
||||||
|
expect(source).toContain('color: var(--sky-text)')
|
||||||
|
expect(source).toContain('shareSelected')
|
||||||
|
expect(source).toContain('deleteDialogOpened = true')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses complete local media fixtures when no browser API port is set', () => {
|
||||||
|
expect(source).toContain("developmentParameters?.has('apiPort')")
|
||||||
|
expect(source).toContain('isDevelopment && !developmentApiEnabled')
|
||||||
|
expect(source).toContain('all: developmentMedia.length')
|
||||||
|
expect(source).toContain('developmentMedia.filter(')
|
||||||
|
expect(source).toContain("mockGalleryImage(\n 'Flower Video'")
|
||||||
|
expect(source).toContain("mockGalleryImage(\n 'Sintel Video'")
|
||||||
|
expect(source).toContain("mockGalleryImage(\n 'Bunny Video'")
|
||||||
|
expect(source).toContain('const additionalPhotos = [')
|
||||||
|
expect(source).toContain("['Campfire', '#c2410c', '#431407', '#fef08a']")
|
||||||
|
expect(source).toContain('id: 13 + index')
|
||||||
|
expect(source).not.toContain('picsum.photos')
|
||||||
|
})
|
||||||
|
})
|
||||||
File diff suppressed because it is too large
Load Diff
+165
-10
@@ -2472,38 +2472,140 @@ let recentCalls = [
|
|||||||
status: 'missed',
|
status: 'missed',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
function mockGalleryImage(title, sky, landscape, accent) {
|
||||||
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 1200"><defs><linearGradient id="sky" x1="0" y1="0" x2="0" y2="1"><stop stop-color="${sky}"/><stop offset="1" stop-color="${accent}"/></linearGradient><linearGradient id="land" x1="0" y1="0" x2="1" y2="1"><stop stop-color="${landscape}"/><stop offset="1" stop-color="#101114"/></linearGradient></defs><rect width="900" height="1200" fill="url(#sky)"/><circle cx="690" cy="260" r="105" fill="#fff" opacity=".72"/><path d="M0 690 210 440 390 650 585 360 900 720V1200H0Z" fill="${landscape}" opacity=".84"/><path d="M0 790 230 620 410 765 650 525 900 770V1200H0Z" fill="url(#land)"/><path d="M360 1200 475 690 560 690 690 1200Z" fill="${accent}" opacity=".48"/><text x="54" y="1100" fill="#fff" font-family="system-ui,sans-serif" font-size="62" font-weight="700">${title}</text><text x="57" y="1160" fill="#fff" opacity=".72" font-family="system-ui,sans-serif" font-size="30">Sky Phone test media</text></svg>`
|
||||||
|
return `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(svg)}`
|
||||||
|
}
|
||||||
|
|
||||||
let mockMedia = [
|
let mockMedia = [
|
||||||
{
|
{
|
||||||
createdAt: Date.now() - 60_000,
|
createdAt: Date.now() - 4 * 60_000,
|
||||||
|
favorite: false,
|
||||||
id: 1,
|
id: 1,
|
||||||
mediaType: 'photo',
|
mediaType: 'photo',
|
||||||
url: 'https://picsum.photos/seed/sky-phone-1/600/800',
|
url: mockGalleryImage('City Night', '#172554', '#111827', '#7c3aed'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
createdAt: Date.now() - 120_000,
|
createdAt: Date.now() - 18 * 60_000,
|
||||||
|
favorite: true,
|
||||||
id: 2,
|
id: 2,
|
||||||
mediaType: 'video',
|
mediaType: 'video',
|
||||||
|
thumbnailUrl: mockGalleryImage('Flower Video', '#7f1d1d', '#365314', '#fb7185'),
|
||||||
url: 'https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4',
|
url: 'https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
createdAt: Date.now() - 180_000,
|
createdAt: Date.now() - 47 * 60_000,
|
||||||
|
favorite: true,
|
||||||
id: 3,
|
id: 3,
|
||||||
mediaType: 'photo',
|
mediaType: 'photo',
|
||||||
url: 'https://picsum.photos/seed/sky-phone-3/800/600',
|
url: mockGalleryImage('Beach Drive', '#0369a1', '#164e63', '#fbbf24'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
createdAt: Date.now() - 240_000,
|
createdAt: Date.now() - 2 * 60 * 60_000,
|
||||||
|
favorite: false,
|
||||||
id: 4,
|
id: 4,
|
||||||
mediaType: 'photo',
|
mediaType: 'photo',
|
||||||
url: 'https://picsum.photos/seed/sky-phone-4/800/600',
|
url: mockGalleryImage('Mountain Road', '#475569', '#334155', '#e2e8f0'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
createdAt: Date.now() - 300_000,
|
createdAt: Date.now() - 5 * 60 * 60_000,
|
||||||
|
favorite: false,
|
||||||
id: 5,
|
id: 5,
|
||||||
mediaType: 'photo',
|
mediaType: 'photo',
|
||||||
url: 'https://picsum.photos/seed/sky-phone-5/800/600',
|
url: mockGalleryImage('Downtown', '#312e81', '#1e293b', '#f472b6'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
createdAt: Date.now() - 8 * 60 * 60_000,
|
||||||
|
favorite: false,
|
||||||
|
id: 6,
|
||||||
|
mediaType: 'video',
|
||||||
|
thumbnailUrl: mockGalleryImage('Sintel Video', '#9a3412', '#431407', '#fdba74'),
|
||||||
|
url: 'https://media.w3.org/2010/05/sintel/trailer.mp4',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
createdAt: Date.now() - 12 * 60 * 60_000,
|
||||||
|
favorite: false,
|
||||||
|
id: 7,
|
||||||
|
mediaType: 'photo',
|
||||||
|
url: mockGalleryImage('Palm Sunset', '#c2410c', '#422006', '#facc15'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
createdAt: Date.now() - 26 * 60 * 60_000,
|
||||||
|
favorite: true,
|
||||||
|
id: 8,
|
||||||
|
mediaType: 'photo',
|
||||||
|
url: mockGalleryImage('Sports Car', '#1f2937', '#111827', '#ef4444'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
createdAt: Date.now() - 31 * 60 * 60_000,
|
||||||
|
favorite: false,
|
||||||
|
id: 9,
|
||||||
|
mediaType: 'photo',
|
||||||
|
url: mockGalleryImage('Boardwalk', '#0e7490', '#713f12', '#67e8f9'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
createdAt: Date.now() - 46 * 60 * 60_000,
|
||||||
|
favorite: false,
|
||||||
|
id: 10,
|
||||||
|
mediaType: 'video',
|
||||||
|
thumbnailUrl: mockGalleryImage('Bunny Video', '#166534', '#14532d', '#86efac'),
|
||||||
|
url: 'https://www.w3schools.com/html/mov_bbb.mp4',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
createdAt: Date.now() - 3 * 86_400_000,
|
||||||
|
favorite: false,
|
||||||
|
id: 11,
|
||||||
|
mediaType: 'photo',
|
||||||
|
url: mockGalleryImage('Vinewood', '#1d4ed8', '#166534', '#f8fafc'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
createdAt: Date.now() - 5 * 86_400_000,
|
||||||
|
favorite: true,
|
||||||
|
id: 12,
|
||||||
|
mediaType: 'photo',
|
||||||
|
url: mockGalleryImage('Friends', '#7e22ce', '#4c1d95', '#f0abfc'),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
mockMedia.push(
|
||||||
|
...[
|
||||||
|
['Airport Lights', '#172554', '#1e3a8a', '#38bdf8'],
|
||||||
|
['Desert Route', '#fb923c', '#7c2d12', '#fde047'],
|
||||||
|
['Harbor Morning', '#0e7490', '#164e63', '#a5f3fc'],
|
||||||
|
['Forest Trail', '#166534', '#14532d', '#bef264'],
|
||||||
|
['Neon Alley', '#581c87', '#1e1b4b', '#f472b6'],
|
||||||
|
['Lake House', '#0369a1', '#3f6212', '#fef08a'],
|
||||||
|
['Snow Pass', '#94a3b8', '#334155', '#f8fafc'],
|
||||||
|
['Night Drive', '#111827', '#312e81', '#22d3ee'],
|
||||||
|
['Canyon View', '#b45309', '#78350f', '#fdba74'],
|
||||||
|
['Green Hills', '#15803d', '#365314', '#86efac'],
|
||||||
|
['Purple Sky', '#6b21a8', '#312e81', '#e879f9'],
|
||||||
|
['Ocean Road', '#0284c7', '#0f766e', '#67e8f9'],
|
||||||
|
['City Park', '#4d7c0f', '#14532d', '#facc15'],
|
||||||
|
['Sunrise Pier', '#ea580c', '#7c2d12', '#fef3c7'],
|
||||||
|
['Rainy Street', '#334155', '#0f172a', '#60a5fa'],
|
||||||
|
['Golden Fields', '#ca8a04', '#713f12', '#fef08a'],
|
||||||
|
['Metro Station', '#1f2937', '#374151', '#f43f5e'],
|
||||||
|
['Island Bay', '#0891b2', '#115e59', '#f0fdfa'],
|
||||||
|
['Cliff Road', '#7c3aed', '#3f3f46', '#c4b5fd'],
|
||||||
|
['Old Town', '#9a3412', '#451a03', '#fed7aa'],
|
||||||
|
['Racing Night', '#991b1b', '#111827', '#fb7185'],
|
||||||
|
['Quiet Beach', '#0ea5e9', '#155e75', '#fde68a'],
|
||||||
|
['Hilltop', '#65a30d', '#3f6212', '#d9f99d'],
|
||||||
|
['Downtown Rain', '#3730a3', '#1e293b', '#93c5fd'],
|
||||||
|
['Coastal Sunset', '#be123c', '#7c2d12', '#fbbf24'],
|
||||||
|
['Country Road', '#854d0e', '#365314', '#fde047'],
|
||||||
|
['Blue Mountains', '#1d4ed8', '#334155', '#bfdbfe'],
|
||||||
|
['Palm Beach', '#0d9488', '#166534', '#fcd34d'],
|
||||||
|
['Skyline', '#4338ca', '#111827', '#a78bfa'],
|
||||||
|
['Campfire', '#c2410c', '#431407', '#fef08a'],
|
||||||
|
].map(([title, sky, landscape, accent], index) => ({
|
||||||
|
createdAt: Date.now() - (6 + index) * 86_400_000,
|
||||||
|
favorite: index % 7 === 0,
|
||||||
|
id: 13 + index,
|
||||||
|
mediaType: 'photo',
|
||||||
|
url: mockGalleryImage(title, sky, landscape, accent),
|
||||||
|
})),
|
||||||
|
)
|
||||||
const weazelNewsCategoryIds = ['official', 'events', 'jobs', 'news', 'business']
|
const weazelNewsCategoryIds = ['official', 'events', 'jobs', 'news', 'business']
|
||||||
let weazelNewsSequence = 8
|
let weazelNewsSequence = 8
|
||||||
let weazelNewsArticles = [
|
let weazelNewsArticles = [
|
||||||
@@ -8265,6 +8367,7 @@ app.post('/api/:endpoint', (request, response) => {
|
|||||||
const fallbackVideo = mockMedia.find((item) => item.mediaType === 'video')
|
const fallbackVideo = mockMedia.find((item) => item.mediaType === 'video')
|
||||||
const media = {
|
const media = {
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
|
favorite: false,
|
||||||
id,
|
id,
|
||||||
mediaType,
|
mediaType,
|
||||||
url:
|
url:
|
||||||
@@ -8353,6 +8456,24 @@ app.post('/api/:endpoint', (request, response) => {
|
|||||||
response.json({ success: true, data: media })
|
response.json({ success: true, data: media })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (endpoint === 'gallery:counts') {
|
||||||
|
response.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
all: mockMedia.length,
|
||||||
|
favoritePhotos: mockMedia.filter(
|
||||||
|
(item) => item.mediaType === 'photo' && item.favorite,
|
||||||
|
).length,
|
||||||
|
favorites: mockMedia.filter((item) => item.favorite).length,
|
||||||
|
favoriteVideos: mockMedia.filter(
|
||||||
|
(item) => item.mediaType === 'video' && item.favorite,
|
||||||
|
).length,
|
||||||
|
photos: mockMedia.filter((item) => item.mediaType === 'photo').length,
|
||||||
|
videos: mockMedia.filter((item) => item.mediaType === 'video').length,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
if (endpoint === 'gallery:list') {
|
if (endpoint === 'gallery:list') {
|
||||||
if (request.body.mockState === 'error') {
|
if (request.body.mockState === 'error') {
|
||||||
response.json({ success: false, error: 'service_unavailable' })
|
response.json({ success: false, error: 'service_unavailable' })
|
||||||
@@ -8362,9 +8483,12 @@ app.post('/api/:endpoint', (request, response) => {
|
|||||||
response.json({ success: true, data: [] })
|
response.json({ success: true, data: [] })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const filtered = request.body.mediaType
|
const typeFiltered = request.body.mediaType
|
||||||
? mockMedia.filter((item) => item.mediaType === request.body.mediaType)
|
? mockMedia.filter((item) => item.mediaType === request.body.mediaType)
|
||||||
: mockMedia
|
: mockMedia
|
||||||
|
const filtered = request.body.favoriteOnly
|
||||||
|
? typeFiltered.filter((item) => item.favorite)
|
||||||
|
: typeFiltered
|
||||||
const offset = Number(request.body.offset) || 0
|
const offset = Number(request.body.offset) || 0
|
||||||
const limit = Number(request.body.limit) || 30
|
const limit = Number(request.body.limit) || 30
|
||||||
response.json({
|
response.json({
|
||||||
@@ -8387,6 +8511,37 @@ app.post('/api/:endpoint', (request, response) => {
|
|||||||
response.json({ success: true })
|
response.json({ success: true })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (endpoint === 'gallery:favorite') {
|
||||||
|
const item = mockMedia.find((entry) => entry.id === Number(request.body.id))
|
||||||
|
if (!item || typeof request.body.favorite !== 'boolean') {
|
||||||
|
response.json({ success: false, error: 'media_not_found' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
item.favorite = request.body.favorite
|
||||||
|
response.json({
|
||||||
|
success: true,
|
||||||
|
data: { favorite: item.favorite, id: item.id },
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (endpoint === 'gallery:delete-many') {
|
||||||
|
const ids = Array.isArray(request.body.ids)
|
||||||
|
? request.body.ids.map(Number).filter(Number.isInteger)
|
||||||
|
: []
|
||||||
|
const deletedIds = mockMedia
|
||||||
|
.filter((item) => ids.includes(item.id))
|
||||||
|
.map((item) => item.id)
|
||||||
|
mockMedia = mockMedia.filter((item) => !deletedIds.includes(item.id))
|
||||||
|
response.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
correlationId: request.body.correlationId,
|
||||||
|
deletedIds,
|
||||||
|
success: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
if (endpoint === 'account:login' || endpoint === 'account:register') {
|
if (endpoint === 'account:login' || endpoint === 'account:register') {
|
||||||
authenticated = true
|
authenticated = true
|
||||||
linkedAccount = {
|
linkedAccount = {
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ const browserDataRequests = [
|
|||||||
['fliptok:discover', { limit: 20 }],
|
['fliptok:discover', { limit: 20 }],
|
||||||
['fliptok:activities', {}],
|
['fliptok:activities', {}],
|
||||||
['gallery:list', {}],
|
['gallery:list', {}],
|
||||||
|
['gallery:counts', {}],
|
||||||
['garage:vehicles', {}],
|
['garage:vehicles', {}],
|
||||||
['garage:valet-state', {}],
|
['garage:valet-state', {}],
|
||||||
['housing:overview', {}],
|
['housing:overview', {}],
|
||||||
@@ -83,6 +84,27 @@ async function expectSuccess(baseUrl, endpoint, body = {}, data = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function verifyStatefulActions(baseUrl) {
|
async function verifyStatefulActions(baseUrl) {
|
||||||
|
let gallery = await expectSuccess(baseUrl, 'gallery:list', {}, true)
|
||||||
|
assert(gallery.length >= 10, 'gallery:list did not include enough test media')
|
||||||
|
assert(
|
||||||
|
gallery.filter((item) => item.mediaType === 'video').length >= 3,
|
||||||
|
'gallery:list did not include enough test videos',
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
gallery
|
||||||
|
.filter((item) => item.mediaType === 'video')
|
||||||
|
.every((item) => typeof item.thumbnailUrl === 'string'),
|
||||||
|
'gallery:list returned a test video without a thumbnail',
|
||||||
|
)
|
||||||
|
await expectSuccess(
|
||||||
|
baseUrl,
|
||||||
|
'gallery:favorite',
|
||||||
|
{ favorite: true, id: gallery[0].id },
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
gallery = await expectSuccess(baseUrl, 'gallery:list', {}, true)
|
||||||
|
assert.equal(gallery.find((item) => item.id === gallery[0].id)?.favorite, true)
|
||||||
|
|
||||||
const memoBootstrap = await expectSuccess(
|
const memoBootstrap = await expectSuccess(
|
||||||
baseUrl,
|
baseUrl,
|
||||||
'development:bootstrap',
|
'development:bootstrap',
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ Locales["en"] = {
|
|||||||
category = "Category",
|
category = "Category",
|
||||||
status = "Publication Status",
|
status = "Publication Status",
|
||||||
cover = "Cover Image",
|
cover = "Cover Image",
|
||||||
chooseCover = "Choose from Gallery",
|
chooseCover = "Choose from Photos",
|
||||||
changeCover = "Change Cover",
|
changeCover = "Change Cover",
|
||||||
removeCover = "Remove Cover",
|
removeCover = "Remove Cover",
|
||||||
coverAlt = "Selected article cover",
|
coverAlt = "Selected article cover",
|
||||||
@@ -269,7 +269,7 @@ Locales["en"] = {
|
|||||||
signInTitle = "Connect your Sky Cloud account", signInBody = "CrewLink uses your private Sky Cloud identity to keep groups and roles available across your phones.", openSettings = "Open Sky Cloud settings",
|
signInTitle = "Connect your Sky Cloud account", signInBody = "CrewLink uses your private Sky Cloud identity to keep groups and roles available across your phones.", openSettings = "Open Sky Cloud settings",
|
||||||
authEyebrow = "Private crew network", authTitle = "Welcome to CrewLink",
|
authEyebrow = "Private crew network", authTitle = "Welcome to CrewLink",
|
||||||
authBody = "Your iFruit email is linked automatically. Use your CrewLink username to continue.",
|
authBody = "Your iFruit email is linked automatically. Use your CrewLink username to continue.",
|
||||||
login = "Log in", register = "Register", ifruitEmail = "iFruit address", gallery = "Gallery", camera = "Camera", backToLogin = "Back to CrewLink login",
|
login = "Log in", register = "Register", ifruitEmail = "iFruit address", gallery = "Photos", camera = "Camera", backToLogin = "Back to CrewLink login",
|
||||||
authErrors = {
|
authErrors = {
|
||||||
no_ifruit_account = "Sign in to your Sky Cloud account in Settings first.", invalid_username = "Use 3-20 letters, numbers, dots, or underscores.",
|
no_ifruit_account = "Sign in to your Sky Cloud account in Settings first.", invalid_username = "Use 3-20 letters, numbers, dots, or underscores.",
|
||||||
profile_not_found = "No CrewLink profile exists for this iFruit email.", profile_exists = "This iFruit email already has a CrewLink profile.",
|
profile_not_found = "No CrewLink profile exists for this iFruit email.", profile_exists = "This iFruit email already has a CrewLink profile.",
|
||||||
@@ -314,7 +314,7 @@ Locales["en"] = {
|
|||||||
flare = {
|
flare = {
|
||||||
name = "Flare", signInTitle = "Sign in to Flare", signInBody = "Flare profiles and matches are linked to your private Sky Cloud account.",
|
name = "Flare", signInTitle = "Sign in to Flare", signInBody = "Flare profiles and matches are linked to your private Sky Cloud account.",
|
||||||
createProfile = "Create your profile", welcome = "Find your spark", welcomeBody = "Build a profile and meet people across Los Santos.",
|
createProfile = "Create your profile", welcome = "Find your spark", welcomeBody = "Build a profile and meet people across Los Santos.",
|
||||||
photo = "Profile photo", profilePhotos = "Profile photos", profilePhotosBody = "Choose up to six photos from Gallery. Your first photo is shown first.", addPhotos = "Add photos", choosePhotos = "Choose from Gallery", primaryPhoto = "Main", removePhoto = "Remove photo {number}",
|
photo = "Profile photo", profilePhotos = "Profile photos", profilePhotosBody = "Choose up to six photos from Photos. Your first photo is shown first.", addPhotos = "Add photos", choosePhotos = "Choose from Photos", primaryPhoto = "Main", removePhoto = "Remove photo {number}",
|
||||||
yourName = "Your name", namePlaceholder = "What should people call you?", age = "Age", bio = "About you", bioPlaceholder = "A short line that starts a conversation...",
|
yourName = "Your name", namePlaceholder = "What should people call you?", age = "Age", bio = "About you", bioPlaceholder = "A short line that starts a conversation...",
|
||||||
gender = "I am", showMe = "Show me", woman = "Woman", man = "Man", nonbinary = "Non-binary", women = "Women", men = "Men", nonbinaryPeople = "Non-binary people", everyone = "Everyone",
|
gender = "I am", showMe = "Show me", woman = "Woman", man = "Man", nonbinary = "Non-binary", women = "Women", men = "Men", nonbinaryPeople = "Non-binary people", everyone = "Everyone",
|
||||||
minimumAge = "Minimum age", maximumAge = "Maximum age", start = "Start exploring", saveProfile = "Save profile", editProfileBody = "Keep your profile fresh and unmistakably you.",
|
minimumAge = "Minimum age", maximumAge = "Maximum age", start = "Start exploring", saveProfile = "Save profile", editProfileBody = "Keep your profile fresh and unmistakably you.",
|
||||||
@@ -336,7 +336,7 @@ Locales["en"] = {
|
|||||||
messagePlaceholder = "Write a message", itsAMatch = "It's a spark!", matchBody = "You and {name} liked each other.", sayHello = "Say hello", keepSwiping = "Keep exploring", newMatchNotification = "You and {sender} found a spark!", newMessageNotification = "New message from {sender}",
|
messagePlaceholder = "Write a message", itsAMatch = "It's a spark!", matchBody = "You and {name} liked each other.", sayHello = "Say hello", keepSwiping = "Keep exploring", newMatchNotification = "You and {sender} found a spark!", newMessageNotification = "New message from {sender}",
|
||||||
lookingFor = { longTerm = "Long-term connection", dates = "Good dates", friends = "New friends" },
|
lookingFor = { longTerm = "Long-term connection", dates = "Good dates", friends = "New friends" },
|
||||||
errors = {
|
errors = {
|
||||||
invalid_profile = "Check your name, age and profile text.", invalid_profile_photos = "Choose up to six photos from your own Gallery.", request_failed = "Flare could not save those changes. Try again.", invalid_target = "This profile is no longer available.", invalid_choice = "That swipe could not be saved.", invalid_discovery = "That Discovery setting is invalid.", discovery_disabled = "Enable Discovery before swiping.",
|
invalid_profile = "Check your name, age and profile text.", invalid_profile_photos = "Choose up to six photos from your own Photos library.", request_failed = "Flare could not save those changes. Try again.", invalid_target = "This profile is no longer available.", invalid_choice = "That swipe could not be saved.", invalid_discovery = "That Discovery setting is invalid.", discovery_disabled = "Enable Discovery before swiping.",
|
||||||
nothing_to_rewind = "There is no recent swipe to rewind.", cannot_rewind_match = "A swipe that created a match cannot be rewound.",
|
nothing_to_rewind = "There is no recent swipe to rewind.", cannot_rewind_match = "A swipe that created a match cannot be rewound.",
|
||||||
match_not_found = "This match is no longer available.", invalid_message = "Write a message before sending.", invalid_attachment = "This attachment is unavailable.", gif_provider_unconfigured = "GIF search is not configured.", gif_provider_unauthorized = "The GIF provider key is invalid.", gif_provider_rate_limited = "GIF search is busy. Try again shortly.", gif_provider_failed = "GIFs are temporarily unavailable.", rate_limited = "Slow down for a moment and try again.", default = "Flare could not complete the request.",
|
match_not_found = "This match is no longer available.", invalid_message = "Write a message before sending.", invalid_attachment = "This attachment is unavailable.", gif_provider_unconfigured = "GIF search is not configured.", gif_provider_unauthorized = "The GIF provider key is invalid.", gif_provider_rate_limited = "GIF search is busy. Try again shortly.", gif_provider_failed = "GIFs are temporarily unavailable.", rate_limited = "Slow down for a moment and try again.", default = "Flare could not complete the request.",
|
||||||
},
|
},
|
||||||
@@ -365,7 +365,7 @@ Locales["en"] = {
|
|||||||
noResults = "No results", noResultsBody = "Try another username, caption, place, or hashtag.",
|
noResults = "No results", noResultsBody = "Try another username, caption, place, or hashtag.",
|
||||||
newPost = "New Post", newStory = "New Story", post = "Post", story = "Story",
|
newPost = "New Post", newStory = "New Story", post = "Post", story = "Story",
|
||||||
choosePhotos = "Choose photos", choosePhoto = "Choose photo",
|
choosePhotos = "Choose photos", choosePhoto = "Choose photo",
|
||||||
choosePhotosHint = "Select up to five photos from Gallery", chooseStoryHint = "Select one photo from Gallery",
|
choosePhotosHint = "Select up to five photos from Photos", chooseStoryHint = "Select one photo from Photos",
|
||||||
selectedPhotos = "{count} selected", changePhotos = "Change selection", caption = "Caption",
|
selectedPhotos = "{count} selected", changePhotos = "Change selection", caption = "Caption",
|
||||||
captionPlaceholder = "Write a caption...", storyTextPlaceholder = "Add a short story text...",
|
captionPlaceholder = "Write a caption...", storyTextPlaceholder = "Add a short story text...",
|
||||||
location = "Location", locationPlaceholder = "Add a place", allowComments = "Allow comments",
|
location = "Location", locationPlaceholder = "Add a place", allowComments = "Allow comments",
|
||||||
@@ -435,7 +435,7 @@ Locales["en"] = {
|
|||||||
createProfile = "Create Feather profile", profileDetailsHint = "Choose how people will recognize you.", displayName = "Display name", displayNamePlaceholder = "Your name", handle = "Username", handlePlaceholder = "your_username", handleHint = "3-30 letters, numbers or underscores",
|
createProfile = "Create Feather profile", profileDetailsHint = "Choose how people will recognize you.", displayName = "Display name", displayNamePlaceholder = "Your name", handle = "Username", handlePlaceholder = "your_username", handleHint = "3-30 letters, numbers or underscores",
|
||||||
bio = "Bio", bioPlaceholder = "What should people know about you?", start = "Join Feather", chooseAvatar = "Choose profile photo", changeAvatar = "Change photo",
|
bio = "Bio", bioPlaceholder = "What should people know about you?", start = "Join Feather", chooseAvatar = "Choose profile photo", changeAvatar = "Change photo",
|
||||||
newPost = "New feather", reply = "Reply", replyingTo = "Replying to @{handle}", quote = "Quote", post = "Post", posting = "Posting...",
|
newPost = "New feather", reply = "Reply", replyingTo = "Replying to @{handle}", quote = "Quote", post = "Post", posting = "Posting...",
|
||||||
composerPlaceholder = "What's happening?", replyPlaceholder = "Post your reply", addPhotos = "Add photos", removePhoto = "Remove photo {number}", charactersLeft = "{count} left", composerAudience = "Everyone can join the conversation", mediaTitle = "Add to your feather", mediaBody = "Share up to four photos from your phone or take a new one.", chooseGallery = "Gallery", chooseGalleryBody = "Choose one or more saved photos", takePhoto = "Camera", takePhotoBody = "Take a new photo now", selectedPhotos = "Selected photos", photoLimit = "You can add up to four photos.",
|
composerPlaceholder = "What's happening?", replyPlaceholder = "Post your reply", addPhotos = "Add photos", removePhoto = "Remove photo {number}", charactersLeft = "{count} left", composerAudience = "Everyone can join the conversation", mediaTitle = "Add to your feather", mediaBody = "Share up to four photos from your phone or take a new one.", chooseGallery = "Photos", chooseGalleryBody = "Choose one or more saved photos", takePhoto = "Camera", takePhotoBody = "Take a new photo now", selectedPhotos = "Selected photos", photoLimit = "You can add up to four photos.",
|
||||||
emptyFeed = "Your timeline is quiet", emptyFeedBody = "Follow people or post the first feather.", emptyExplore = "No results found", emptyExploreBody = "Try another search or explore a topic.", noTrendingHashtags = "No hashtags yet", noTrendingHashtagsBody = "Hashtags from published posts will appear here.",
|
emptyFeed = "Your timeline is quiet", emptyFeedBody = "Follow people or post the first feather.", emptyExplore = "No results found", emptyExploreBody = "Try another search or explore a topic.", noTrendingHashtags = "No hashtags yet", noTrendingHashtagsBody = "Hashtags from published posts will appear here.",
|
||||||
happeningNow = "What's happening", trends = "Trends for you", trendingIn = "Trending in Los Santos", openTopic = "View conversation", postsCount = "{count} posts",
|
happeningNow = "What's happening", trends = "Trends for you", trendingIn = "Trending in Los Santos", openTopic = "View conversation", postsCount = "{count} posts",
|
||||||
exploreTabs = { explore = "Explore", trending = "Trends", news = "News" },
|
exploreTabs = { explore = "Explore", trending = "Trends", news = "News" },
|
||||||
@@ -455,7 +455,7 @@ Locales["en"] = {
|
|||||||
originalSound = "original sound", save = "Save", home = "Home", discover = "Discover", create = "Create", activity = "Activity", profile = "Profile", navigation = "FlipTok navigation",
|
originalSound = "original sound", save = "Save", home = "Home", discover = "Discover", create = "Create", activity = "Activity", profile = "Profile", navigation = "FlipTok navigation",
|
||||||
emptyFeed = "No videos yet", emptyFeedBody = "Follow creators or post the first FlipTok.", searchPlaceholder = "Search creators and videos", clearSearch = "Clear search",
|
emptyFeed = "No videos yet", emptyFeedBody = "Follow creators or post the first FlipTok.", searchPlaceholder = "Search creators and videos", clearSearch = "Clear search",
|
||||||
noActivity = "No activity yet", followers = "Followers", videos = "Videos", emptyBio = "No bio yet.", editProfile = "Edit profile",
|
noActivity = "No activity yet", followers = "Followers", videos = "Videos", emptyBio = "No bio yet.", editProfile = "Edit profile",
|
||||||
newVideo = "New FlipTok", createTitle = "Create your next FlipTok", createBody = "Choose a clip, set the cover and sound, then decide who can watch it.", chooseVideo = "Choose a video", chooseVideoHint = "Select one from Gallery", changeVideo = "Change",
|
newVideo = "New FlipTok", createTitle = "Create your next FlipTok", createBody = "Choose a clip, set the cover and sound, then decide who can watch it.", chooseVideo = "Choose a video", chooseVideoHint = "Select one from Photos", changeVideo = "Change",
|
||||||
caption = "Caption", captionPlaceholder = "Write a caption...", location = "Add location", whoCanWatch = "Who can watch", public = "Everyone",
|
caption = "Caption", captionPlaceholder = "Write a caption...", location = "Add location", whoCanWatch = "Who can watch", public = "Everyone",
|
||||||
followersOnly = "Followers", private = "Only me", allowComments = "Allow comments", saveDraft = "Drafts", publishing = "Posting...", post = "Post",
|
followersOnly = "Followers", private = "Only me", allowComments = "Allow comments", saveDraft = "Drafts", publishing = "Posting...", post = "Post",
|
||||||
draftSaved = "Draft saved.", published = "Your FlipTok is live.", linkCopied = "Video link copied.", reported = "Report submitted.", blocked = "Creator blocked.",
|
draftSaved = "Draft saved.", published = "Your FlipTok is live.", linkCopied = "Video link copied.", reported = "Report submitted.", blocked = "Creator blocked.",
|
||||||
@@ -468,7 +468,7 @@ Locales["en"] = {
|
|||||||
customSoundFormats = "YouTube, YouTube Music, Shorts, MP3, M4A, AAC, OGG, OPUS, WAV, or WEBM", useCustomSound = "Use sound", loadingSound = "Loading sound", invalidCustomSoundLink = "Use a YouTube link or a direct public HTTPS audio-file link.", customSoundLoadFailed = "This sound could not be loaded.",
|
customSoundFormats = "YouTube, YouTube Music, Shorts, MP3, M4A, AAC, OGG, OPUS, WAV, or WEBM", useCustomSound = "Use sound", loadingSound = "Loading sound", invalidCustomSoundLink = "Use a YouTube link or a direct public HTTPS audio-file link.", customSoundLoadFailed = "This sound could not be loaded.",
|
||||||
trimAndCover = "Trim & cover", trimStart = "Start", trimEnd = "End", coverFrame = "Cover", originalVolume = "Original sound", musicVolume = "Music",
|
trimAndCover = "Trim & cover", trimStart = "Start", trimEnd = "End", coverFrame = "Cover", originalVolume = "Original sound", musicVolume = "Music",
|
||||||
moderation = "Moderation", reports = "Open reports", noReports = "No open reports", removeVideo = "Remove video", dismissReport = "Dismiss",
|
moderation = "Moderation", reports = "Open reports", noReports = "No open reports", removeVideo = "Remove video", dismissReport = "Dismiss",
|
||||||
done = "Done", displayName = "Name", username = "Username", bio = "Bio", accountType = "Account type", profilePhoto = "Profile photo", changePhoto = "Change photo", chooseFromGallery = "Gallery", takePhoto = "Camera", removePhoto = "Remove photo", noConnections = "No profiles to show",
|
done = "Done", displayName = "Name", username = "Username", bio = "Bio", accountType = "Account type", profilePhoto = "Profile photo", changePhoto = "Change photo", chooseFromGallery = "Photos", takePhoto = "Camera", removePhoto = "Remove photo", noConnections = "No profiles to show",
|
||||||
authTitle = "Your FlipTok account", login = "Sign In", register = "Register", createAccount = "Create Account", logout = "Sign Out",
|
authTitle = "Your FlipTok account", login = "Sign In", register = "Register", createAccount = "Create Account", logout = "Sign Out",
|
||||||
loginBody = "Sign in to continue with your videos, follows, and saved posts.", registerBody = "Create a private FlipTok login for this profile.",
|
loginBody = "Sign in to continue with your videos, follows, and saved posts.", registerBody = "Create a private FlipTok login for this profile.",
|
||||||
password = "Password", confirmPassword = "Confirm password", passwordsMismatch = "The passwords do not match.",
|
password = "Password", confirmPassword = "Confirm password", passwordsMismatch = "The passwords do not match.",
|
||||||
@@ -850,7 +850,7 @@ Locales["en"] = {
|
|||||||
noRecents = "No Recent Calls", noContacts = "No Contacts", searchContacts = "Search Contacts", favorites = "Favorites",
|
noRecents = "No Recent Calls", noContacts = "No Contacts", searchContacts = "Search Contacts", favorites = "Favorites",
|
||||||
addContact = "New Contact", newContact = "New", editContact = "Edit Contact", contactName = "Name", firstName = "First Name", lastName = "Last Name", companyOrGroup = "Company or Group", phoneNumber = "Phone Number",
|
addContact = "New Contact", newContact = "New", editContact = "Edit Contact", contactName = "Name", firstName = "First Name", lastName = "Last Name", companyOrGroup = "Company or Group", phoneNumber = "Phone Number",
|
||||||
officialContact = "Official company contact",
|
officialContact = "Official company contact",
|
||||||
choosePhoto = "Choose Contact Photo", chooseGallery = "Gallery", takePhoto = "Camera", removePhoto = "Remove Photo",
|
choosePhoto = "Choose Contact Photo", chooseGallery = "Photos", takePhoto = "Camera", removePhoto = "Remove Photo",
|
||||||
message = "Message", video = "Video", mail = "Mail", defaultLabel = "Default", privateLabel = "Private",
|
message = "Message", video = "Video", mail = "Mail", defaultLabel = "Default", privateLabel = "Private",
|
||||||
contactCard = "Contact Card", removeContact = "Remove Contact", returnToCall = "Return to Call", mobile = "Mobile", messagesProfile = "Messages", notes = "Notes",
|
contactCard = "Contact Card", removeContact = "Remove Contact", returnToCall = "Return to Call", mobile = "Mobile", messagesProfile = "Messages", notes = "Notes",
|
||||||
sendMessage = "Send Message", shareContact = "Share Contact", addFavorite = "Add to Favorites", removeFavorite = "Remove from Favorites",
|
sendMessage = "Send Message", shareContact = "Share Contact", addFavorite = "Add to Favorites", removeFavorite = "Remove from Favorites",
|
||||||
@@ -1097,7 +1097,7 @@ Locales["en"] = {
|
|||||||
driverModeBody = "Accept ride requests and track your earnings", completedRides = "Completed",
|
driverModeBody = "Accept ride requests and track your earnings", completedRides = "Completed",
|
||||||
cancelledRides = "Cancelled", acceptance = "Acceptance", rating = "Rating", notAvailable = "N/A",
|
cancelledRides = "Cancelled", acceptance = "Acceptance", rating = "Rating", notAvailable = "N/A",
|
||||||
profile = "Your profile", editProfile = "Edit Profile", editProfileBody = "Change your public name and profile photo",
|
profile = "Your profile", editProfile = "Edit Profile", editProfileBody = "Change your public name and profile photo",
|
||||||
profilePhoto = "Profile photo", gallery = "Gallery", camera = "Camera", profileDetails = "Profile details",
|
profilePhoto = "Profile photo", gallery = "Photos", camera = "Camera", profileDetails = "Profile details",
|
||||||
profileName = "Name", profileNamePlaceholder = "Your SkyRide name", removeProfilePhoto = "Remove profile photo",
|
profileName = "Name", profileNamePlaceholder = "Your SkyRide name", removeProfilePhoto = "Remove profile photo",
|
||||||
saveProfile = "Save Profile", profileSaved = "Your SkyRide profile was updated.",
|
saveProfile = "Save Profile", profileSaved = "Your SkyRide profile was updated.",
|
||||||
memberSince = "SkyRide member since {date}", rideComplete = "Ride Complete", rateRide = "Rate your ride",
|
memberSince = "SkyRide member since {date}", rideComplete = "Ride Complete", rateRide = "Rate your ride",
|
||||||
@@ -1257,8 +1257,8 @@ Locales["en"] = {
|
|||||||
name = "Camera", flash = "Flash", flip = "Flip camera", landscape = "Switch to landscape",
|
name = "Camera", flash = "Flash", flip = "Flip camera", landscape = "Switch to landscape",
|
||||||
portrait = "Switch to portrait", photo = "Photo", video = "Video", microphoneOn = "Microphone on", microphoneOff = "Microphone muted",
|
portrait = "Switch to portrait", photo = "Photo", video = "Video", microphoneOn = "Microphone on", microphoneOff = "Microphone muted",
|
||||||
focusHelp = "Space for movement", returnHelp = "Space to return", uploading = "{count} uploading",
|
focusHelp = "Space for movement", returnHelp = "Space to return", uploading = "{count} uploading",
|
||||||
saving = "Saving video...", openGallery = "Open Gallery", takePhoto = "Take photo",
|
saving = "Saving video...", openGallery = "Open Photos", takePhoto = "Take photo",
|
||||||
startRecording = "Start recording", stopRecording = "Stop recording", saved = "Saved to Gallery.",
|
startRecording = "Start recording", stopRecording = "Stop recording", saved = "Saved to Photos.",
|
||||||
zoom = "Set camera zoom to {zoom}",
|
zoom = "Set camera zoom to {zoom}",
|
||||||
errors = {
|
errors = {
|
||||||
cancelled = "Capture cancelled.", capture_failed = "Unable to capture the game view.",
|
cancelled = "Capture cancelled.", capture_failed = "Unable to capture the game view.",
|
||||||
@@ -1389,7 +1389,7 @@ Locales["en"] = {
|
|||||||
createProfile = "Create your CityMarkt profile", editProfile = "Edit profile",
|
createProfile = "Create your CityMarkt profile", editProfile = "Edit profile",
|
||||||
profileIntro = "Your iFruit email stays linked to this profile.", profileEmail = "iFruit email",
|
profileIntro = "Your iFruit email stays linked to this profile.", profileEmail = "iFruit email",
|
||||||
displayName = "Display name", profileBio = "About you", saveProfile = "Save profile",
|
displayName = "Display name", profileBio = "About you", saveProfile = "Save profile",
|
||||||
cancel = "Cancel", profileSaved = "Your profile was saved.", chooseGallery = "Gallery",
|
cancel = "Cancel", profileSaved = "Your profile was saved.", chooseGallery = "Photos",
|
||||||
takePhoto = "Camera", removeProfilePhoto = "Remove photo",
|
takePhoto = "Camera", removeProfilePhoto = "Remove photo",
|
||||||
phone = "Phone", contactSeller = "Contact seller", messagePlaceholder = "Hi, is this still available?",
|
phone = "Phone", contactSeller = "Contact seller", messagePlaceholder = "Hi, is this still available?",
|
||||||
signInToMessage = "Sign in to your Sky Cloud account to send a message.",
|
signInToMessage = "Sign in to your Sky Cloud account to send a message.",
|
||||||
@@ -1450,11 +1450,11 @@ Locales["en"] = {
|
|||||||
createProfile = "Create your profile", editProfile = "Edit profile", profileSetupBody = "Your iFruit email is linked automatically.",
|
createProfile = "Create your profile", editProfile = "Edit profile", profileSetupBody = "Your iFruit email is linked automatically.",
|
||||||
profileEmail = "iFruit email", profileHandle = "Username", profileHandlePlaceholder = "your.name",
|
profileEmail = "iFruit email", profileHandle = "Username", profileHandlePlaceholder = "your.name",
|
||||||
profileHandleHint = "Use 3-24 lowercase letters, numbers, dots or underscores.", profileBio = "Bio",
|
profileHandleHint = "Use 3-24 lowercase letters, numbers, dots or underscores.", profileBio = "Bio",
|
||||||
profileBioPlaceholder = "Tell the city a little about yourself...", profilePhoto = "Profile photo", profilePhotoHint = "Choose a photo from your Gallery or take a new one.", removeProfilePhoto = "Remove photo", profileSave = "Save profile", profileCancel = "Cancel", profileSaved = "Your profile was saved.",
|
profileBioPlaceholder = "Tell the city a little about yourself...", profilePhoto = "Profile photo", profilePhotoHint = "Choose a photo from Photos or take a new one.", removeProfilePhoto = "Remove photo", profileSave = "Save profile", profileCancel = "Cancel", profileSaved = "Your profile was saved.",
|
||||||
location = "Location", sharedFrom = "Shared from CityMarkt", openCityMarkt = "Open CityMarkt listing",
|
location = "Location", sharedFrom = "Shared from CityMarkt", openCityMarkt = "Open CityMarkt listing",
|
||||||
newPost = "New local post", shareWithCity = "Share with the city", publish = "Publish", published = "Your post is live.", deleted = "Post deleted.",
|
newPost = "New local post", shareWithCity = "Share with the city", publish = "Publish", published = "Your post is live.", deleted = "Post deleted.",
|
||||||
title = "Title", body = "Your story", category = "Category", titlePlaceholder = "What should people know?", bodyPlaceholder = "Add details, a recommendation or directions...",
|
title = "Title", body = "Your story", category = "Category", titlePlaceholder = "What should people know?", bodyPlaceholder = "Add details, a recommendation or directions...",
|
||||||
photos = "Photos", optional = "optional", camera = "Camera", gallery = "Gallery", photoLimit = "You can add up to six photos.",
|
photos = "Photos", optional = "optional", camera = "Camera", gallery = "Photos", photoLimit = "You can add up to six photos.",
|
||||||
cityMarktShare = "Share to Local Pages", cityMarktShared = "Shared to Local Pages.", cityMarktShareHint = "One CityMarkt share per day",
|
cityMarktShare = "Share to Local Pages", cityMarktShared = "Shared to Local Pages.", cityMarktShareHint = "One CityMarkt share per day",
|
||||||
cityMarktAlreadyShared = "Already published", cityMarktOpenSharedHint = "Open the Local Pages post",
|
cityMarktAlreadyShared = "Already published", cityMarktOpenSharedHint = "Open the Local Pages post",
|
||||||
cityMarktAppMissing = "Local Pages is not installed", cityMarktInstallHint = "Install Local Pages to share this listing",
|
cityMarktAppMissing = "Local Pages is not installed", cityMarktInstallHint = "Install Local Pages to share this listing",
|
||||||
@@ -1592,7 +1592,7 @@ Locales["en"] = {
|
|||||||
destinations = "Share destinations", newMessage = "New Message", sentToChat = "Sent to chat.",
|
destinations = "Share destinations", newMessage = "New Message", sentToChat = "Sent to chat.",
|
||||||
shareProfile = "Share Profile", chooseConversation = "Choose a conversation",
|
shareProfile = "Share Profile", chooseConversation = "Choose a conversation",
|
||||||
kinds = {
|
kinds = {
|
||||||
contact = "Contact", document = "Document", link = "Link", location = "Location",
|
contact = "Contact", document = "Document", link = "Link", location = "Location", media = "Media",
|
||||||
note = "Note", photo = "Photo", playlist = "Playlist", post = "Post",
|
note = "Note", photo = "Photo", playlist = "Playlist", post = "Post",
|
||||||
profile = "Profile", text = "Message", track = "Track", video = "Video",
|
profile = "Profile", text = "Message", track = "Track", video = "Video",
|
||||||
},
|
},
|
||||||
@@ -1616,9 +1616,31 @@ Locales["en"] = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
photos = {
|
photos = {
|
||||||
name = "Gallery", count = "{count} items", loading = "Loading Gallery...",
|
name = "Photos", library = "Library",
|
||||||
|
counts = {
|
||||||
|
all = "{count} Objects", allOne = "{count} Object",
|
||||||
|
favorite = "{count} Favorite", favorites = "{count} Favorites",
|
||||||
|
photo = "{count} Photo", photos = "{count} Photos",
|
||||||
|
video = "{count} Video", videos = "{count} Videos",
|
||||||
|
},
|
||||||
|
sorting = {
|
||||||
|
action = "Sort Photos", title = "Sort Order",
|
||||||
|
show = "Show", allItems = "All Items", favorites = "Favorites",
|
||||||
|
newestFirst = "Newest First", oldestFirst = "Oldest First",
|
||||||
|
},
|
||||||
|
selection = {
|
||||||
|
action = "Select", selected = "{count} Selected",
|
||||||
|
share = "Share Selected", shareTitle = "{count} Media Items",
|
||||||
|
shareCopy = "{count} photos and videos shared from Photos.",
|
||||||
|
delete = "Delete Selected", deleteTitle = "Delete Selected Media?",
|
||||||
|
deleteBody = "The selected photos and videos will be permanently deleted.",
|
||||||
|
deleted = "{count} media items deleted.", limit = "You can select up to 50 media items.",
|
||||||
|
},
|
||||||
|
loading = "Loading Photos...",
|
||||||
emptyTitle = "No Photos or Videos", emptyBody = "Captures from Camera will appear here.",
|
emptyTitle = "No Photos or Videos", emptyBody = "Captures from Camera will appear here.",
|
||||||
photo = "Photo", video = "Video", photoAlt = "Gallery photo", videoAlt = "Gallery video",
|
photo = "Photo", video = "Video", photoAlt = "Photo library image", videoAlt = "Photo library video",
|
||||||
|
today = "Today", yesterday = "Yesterday",
|
||||||
|
addFavorite = "Add to Favorites", removeFavorite = "Remove from Favorites",
|
||||||
delete = "Delete media", deleteTitle = "Delete Media?",
|
delete = "Delete media", deleteTitle = "Delete Media?",
|
||||||
deleteBody = "This photo or video will be permanently deleted.", deleted = "Media deleted.",
|
deleteBody = "This photo or video will be permanently deleted.", deleted = "Media deleted.",
|
||||||
zoomIn = "Zoom in", zoomOut = "Zoom out", resetZoom = "Reset zoom",
|
zoomIn = "Zoom in", zoomOut = "Zoom out", resetZoom = "Reset zoom",
|
||||||
@@ -1636,7 +1658,7 @@ Locales["en"] = {
|
|||||||
cancelled = "The media action was cancelled.", capture_failed = "Unable to capture the game view.",
|
cancelled = "The media action was cancelled.", capture_failed = "Unable to capture the game view.",
|
||||||
invalid_import_request = "The import request is invalid.", invalid_import_media = "This media item cannot be imported.",
|
invalid_import_request = "The import request is invalid.", invalid_import_media = "This media item cannot be imported.",
|
||||||
invalid_media_type = "The media type is invalid.", invalid_upload = "The upload could not be verified.",
|
invalid_media_type = "The media type is invalid.", invalid_upload = "The upload could not be verified.",
|
||||||
invalid_upload_token = "The upload session is no longer valid.", missing_config = "Gallery uploads are not configured.",
|
invalid_upload_token = "The upload session is no longer valid.", missing_config = "Photos uploads are not configured.",
|
||||||
import_media_not_allowed = "This media item is not allowed.", import_media_too_large = "This media item is too large.",
|
import_media_not_allowed = "This media item is not allowed.", import_media_too_large = "This media item is too large.",
|
||||||
import_media_unavailable = "This media item is no longer available.", import_provider_failed = "The website could not load its media.",
|
import_media_unavailable = "This media item is no longer available.", import_provider_failed = "The website could not load its media.",
|
||||||
import_provider_unauthorized = "The website credentials are invalid.", import_source_not_found = "This website is not registered.",
|
import_provider_unauthorized = "The website credentials are invalid.", import_source_not_found = "This website is not registered.",
|
||||||
@@ -1645,7 +1667,7 @@ Locales["en"] = {
|
|||||||
import_url_unavailable = "The linked media could not be reached.", import_size_unavailable = "The website did not provide the media size.",
|
import_url_unavailable = "The linked media could not be reached.", import_size_unavailable = "The website did not provide the media size.",
|
||||||
not_found = "The media item no longer exists.", owner_changed = "The active phone account changed.",
|
not_found = "The media item no longer exists.", owner_changed = "The active phone account changed.",
|
||||||
operation_in_progress = "Another media operation is already in progress.",
|
operation_in_progress = "Another media operation is already in progress.",
|
||||||
rate_limited = "Too many media actions. Try again shortly.", request_failed = "The Gallery request failed.",
|
rate_limited = "Too many media actions. Try again shortly.", request_failed = "The Photos request failed.",
|
||||||
request_timeout = "The media service timed out.", unsupported = "This media format is not supported.",
|
request_timeout = "The media service timed out.", unsupported = "This media format is not supported.",
|
||||||
upload_failed = "The media upload failed.", upload_timeout = "The media upload timed out.",
|
upload_failed = "The media upload failed.", upload_timeout = "The media upload timed out.",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -453,6 +453,15 @@ RegisterNUICallback("gallery:delete", function(data, cb)
|
|||||||
cb({ success = true })
|
cb({ success = true })
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
RegisterNUICallback("gallery:delete-many", function(data, cb)
|
||||||
|
if type(data) ~= "table" then
|
||||||
|
cb({ success = false, error = "invalid_request" })
|
||||||
|
return
|
||||||
|
end
|
||||||
|
TriggerServerEvent("sky_phone:media:delete-many", data)
|
||||||
|
cb({ success = true })
|
||||||
|
end)
|
||||||
|
|
||||||
RegisterNUICallback("memos:requestUpload", function(data, cb)
|
RegisterNUICallback("memos:requestUpload", function(data, cb)
|
||||||
if type(data) ~= "table" then
|
if type(data) ~= "table" then
|
||||||
cb({ success = false, error = "invalid_request" })
|
cb({ success = false, error = "invalid_request" })
|
||||||
@@ -501,6 +510,10 @@ RegisterNetEvent("sky_phone:media:delete-result", function(data)
|
|||||||
SendNUIMessage({ type = "media:deleteResult", data = data })
|
SendNUIMessage({ type = "media:deleteResult", data = data })
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
RegisterNetEvent("sky_phone:media:delete-many-result", function(data)
|
||||||
|
SendNUIMessage({ type = "media:deleteManyResult", data = data })
|
||||||
|
end)
|
||||||
|
|
||||||
RegisterNetEvent("sky_phone:memos:upload-ready", function(data)
|
RegisterNetEvent("sky_phone:memos:upload-ready", function(data)
|
||||||
SendNUIMessage({ type = "memos:uploadReady", data = data })
|
SendNUIMessage({ type = "memos:uploadReady", data = data })
|
||||||
end)
|
end)
|
||||||
|
|||||||
@@ -274,6 +274,8 @@ local server_callbacks = {
|
|||||||
"flare:thread",
|
"flare:thread",
|
||||||
"flare:send",
|
"flare:send",
|
||||||
"gallery:list",
|
"gallery:list",
|
||||||
|
"gallery:counts",
|
||||||
|
"gallery:favorite",
|
||||||
"media:config",
|
"media:config",
|
||||||
"media:import:sources",
|
"media:import:sources",
|
||||||
"media:import:list",
|
"media:import:list",
|
||||||
|
|||||||
@@ -386,6 +386,7 @@ local schema = {
|
|||||||
collation = "ascii_bin",
|
collation = "ascii_bin",
|
||||||
},
|
},
|
||||||
{ name = "verified_at", type = "DATETIME NULL" },
|
{ name = "verified_at", type = "DATETIME NULL" },
|
||||||
|
{ name = "favorite", type = "TINYINT(1) NOT NULL DEFAULT 0" },
|
||||||
{ name = "created_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP" },
|
{ name = "created_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP" },
|
||||||
},
|
},
|
||||||
primaryKey = "id",
|
primaryKey = "id",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ local valid_kinds = {
|
|||||||
document = true,
|
document = true,
|
||||||
link = true,
|
link = true,
|
||||||
location = true,
|
location = true,
|
||||||
|
media = true,
|
||||||
note = true,
|
note = true,
|
||||||
photo = true,
|
photo = true,
|
||||||
playlist = true,
|
playlist = true,
|
||||||
@@ -671,6 +672,45 @@ local function sanitize_payload(source, device, data)
|
|||||||
payload.title = note.title ~= "" and note.title or title
|
payload.title = note.title ~= "" and note.title or title
|
||||||
payload.copyText = plain_note_body(note.body)
|
payload.copyText = plain_note_body(note.body)
|
||||||
payload.meta = { body = note.body, title = note.title }
|
payload.meta = { body = note.body, title = note.title }
|
||||||
|
elseif data.kind == "media" and app_id == "photos" and type(data.meta) == "table" and type(data.meta.mediaIds) == "table" then
|
||||||
|
local media_ids = {}
|
||||||
|
local seen_ids = {}
|
||||||
|
for _, value in ipairs(data.meta.mediaIds) do
|
||||||
|
local media_id = tonumber(value)
|
||||||
|
if media_id and media_id > 0 and media_id == math.floor(media_id) and not seen_ids[media_id] then
|
||||||
|
seen_ids[media_id] = true
|
||||||
|
media_ids[#media_ids + 1] = media_id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if #media_ids < 1 or #media_ids > 50 then
|
||||||
|
return nil, "invalid_payload"
|
||||||
|
end
|
||||||
|
local placeholders = {}
|
||||||
|
local media_params = {}
|
||||||
|
for _, media_id in ipairs(media_ids) do
|
||||||
|
placeholders[#placeholders + 1] = "?"
|
||||||
|
media_params[#media_params + 1] = media_id
|
||||||
|
end
|
||||||
|
append_params(media_params, owner_params)
|
||||||
|
local media_rows = Bridge.Database.Query(([[
|
||||||
|
SELECT `id`, `url`, `media_type` AS `mediaType`
|
||||||
|
FROM `sky_phone_media`
|
||||||
|
WHERE `id` IN (%s) AND %s AND `media_type` IN ('photo', 'video')
|
||||||
|
ORDER BY `created_at` ASC, `id` ASC
|
||||||
|
]]):format(table.concat(placeholders, ", "), condition), media_params)
|
||||||
|
if #media_rows ~= #media_ids then
|
||||||
|
return nil, "not_owned"
|
||||||
|
end
|
||||||
|
local items = {}
|
||||||
|
for _, row in ipairs(media_rows) do
|
||||||
|
items[#items + 1] = {
|
||||||
|
id = tonumber(row.id),
|
||||||
|
mediaType = row.mediaType,
|
||||||
|
url = row.url,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
payload.imageUrl = items[1].url
|
||||||
|
payload.meta = { items = items }
|
||||||
elseif data.kind == "photo" or data.kind == "video" then
|
elseif data.kind == "photo" or data.kind == "video" then
|
||||||
local url, media_error = SkyPhoneMedia.ResolveOwnedMedia(source, payload.id, data.kind)
|
local url, media_error = SkyPhoneMedia.ResolveOwnedMedia(source, payload.id, data.kind)
|
||||||
if not url then
|
if not url then
|
||||||
|
|||||||
@@ -305,6 +305,15 @@ local function delete_result(source, correlation_id, success, error_code, media_
|
|||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function delete_many_result(source, correlation_id, success, error_code, deleted_ids)
|
||||||
|
TriggerClientEvent("sky_phone:media:delete-many-result", source, {
|
||||||
|
correlationId = correlation_id,
|
||||||
|
success = success,
|
||||||
|
error = error_code,
|
||||||
|
deletedIds = deleted_ids,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
local function parse_metadata(value)
|
local function parse_metadata(value)
|
||||||
if type(value) == "table" then
|
if type(value) == "table" then
|
||||||
return value
|
return value
|
||||||
@@ -412,10 +421,13 @@ Bridge.Callbacks.Register("sky_phone:gallery:list", function(source, data)
|
|||||||
condition = condition .. " AND `media_type` = ?"
|
condition = condition .. " AND `media_type` = ?"
|
||||||
params[#params + 1] = media_type
|
params[#params + 1] = media_type
|
||||||
end
|
end
|
||||||
|
if data.favoriteOnly == true then
|
||||||
|
condition = condition .. " AND `favorite` = 1"
|
||||||
|
end
|
||||||
params[#params + 1] = limit
|
params[#params + 1] = limit
|
||||||
params[#params + 1] = offset
|
params[#params + 1] = offset
|
||||||
local rows = Bridge.Database.Query(([[
|
local rows = Bridge.Database.Query(([[
|
||||||
SELECT `id`, `url`, `media_type` AS `mediaType`,
|
SELECT `id`, `url`, `media_type` AS `mediaType`, `favorite`,
|
||||||
UNIX_TIMESTAMP(`created_at`) * 1000 AS `createdAt`
|
UNIX_TIMESTAMP(`created_at`) * 1000 AS `createdAt`
|
||||||
FROM `sky_phone_media`
|
FROM `sky_phone_media`
|
||||||
WHERE %s
|
WHERE %s
|
||||||
@@ -425,10 +437,85 @@ Bridge.Callbacks.Register("sky_phone:gallery:list", function(source, data)
|
|||||||
for _, row in ipairs(rows) do
|
for _, row in ipairs(rows) do
|
||||||
row.id = tonumber(row.id)
|
row.id = tonumber(row.id)
|
||||||
row.createdAt = tonumber(row.createdAt) or 0
|
row.createdAt = tonumber(row.createdAt) or 0
|
||||||
|
row.favorite = tonumber(row.favorite) == 1
|
||||||
end
|
end
|
||||||
return { success = true, data = rows }
|
return { success = true, data = rows }
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
Bridge.Callbacks.Register("sky_phone:gallery:favorite", function(source, data)
|
||||||
|
if not SkyPhone.AllowOperation(source, "media_favorite", 60, 60)
|
||||||
|
or type(data) ~= "table"
|
||||||
|
or type(data.favorite) ~= "boolean"
|
||||||
|
then
|
||||||
|
return { success = false, error = "invalid_request" }
|
||||||
|
end
|
||||||
|
local media_id = tonumber(data.id)
|
||||||
|
if not media_id or media_id < 1 or media_id ~= math.floor(media_id) then
|
||||||
|
return { success = false, error = "invalid_request" }
|
||||||
|
end
|
||||||
|
local owner, error_response = session_owner(source)
|
||||||
|
if not owner then
|
||||||
|
return error_response
|
||||||
|
end
|
||||||
|
local condition, owner_params = owner_condition(owner)
|
||||||
|
local params = { media_id }
|
||||||
|
for _, value in ipairs(owner_params) do
|
||||||
|
params[#params + 1] = value
|
||||||
|
end
|
||||||
|
local rows = Bridge.Database.Query(([[
|
||||||
|
SELECT `id`
|
||||||
|
FROM `sky_phone_media`
|
||||||
|
WHERE `id` = ? AND %s AND `media_type` IN ('photo', 'video')
|
||||||
|
LIMIT 1
|
||||||
|
]]):format(condition), params)
|
||||||
|
if not rows[1] then
|
||||||
|
return { success = false, error = "media_not_found" }
|
||||||
|
end
|
||||||
|
local update_params = { data.favorite and 1 or 0, media_id }
|
||||||
|
for _, value in ipairs(owner_params) do
|
||||||
|
update_params[#update_params + 1] = value
|
||||||
|
end
|
||||||
|
Bridge.Database.Query(
|
||||||
|
("UPDATE `sky_phone_media` SET `favorite` = ? WHERE `id` = ? AND %s"):format(condition),
|
||||||
|
update_params
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
success = true,
|
||||||
|
data = { id = media_id, favorite = data.favorite },
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
|
||||||
|
Bridge.Callbacks.Register("sky_phone:gallery:counts", function(source)
|
||||||
|
local owner, error_response = session_owner(source)
|
||||||
|
if not owner then
|
||||||
|
return error_response
|
||||||
|
end
|
||||||
|
local condition, params = owner_condition(owner)
|
||||||
|
local rows = Bridge.Database.Query(([[
|
||||||
|
SELECT
|
||||||
|
COUNT(*) AS `all_count`,
|
||||||
|
COUNT(CASE WHEN `favorite` = 1 THEN 1 END) AS `favorite_count`,
|
||||||
|
COUNT(CASE WHEN `media_type` = 'photo' AND `favorite` = 1 THEN 1 END) AS `favorite_photo_count`,
|
||||||
|
COUNT(CASE WHEN `media_type` = 'video' AND `favorite` = 1 THEN 1 END) AS `favorite_video_count`,
|
||||||
|
COUNT(CASE WHEN `media_type` = 'photo' THEN 1 END) AS `photo_count`,
|
||||||
|
COUNT(CASE WHEN `media_type` = 'video' THEN 1 END) AS `video_count`
|
||||||
|
FROM `sky_phone_media`
|
||||||
|
WHERE %s AND `media_type` IN ('photo', 'video')
|
||||||
|
]]):format(condition), params)
|
||||||
|
local counts = rows[1] or {}
|
||||||
|
return {
|
||||||
|
success = true,
|
||||||
|
data = {
|
||||||
|
all = tonumber(counts.all_count) or 0,
|
||||||
|
favoritePhotos = tonumber(counts.favorite_photo_count) or 0,
|
||||||
|
favorites = tonumber(counts.favorite_count) or 0,
|
||||||
|
favoriteVideos = tonumber(counts.favorite_video_count) or 0,
|
||||||
|
photos = tonumber(counts.photo_count) or 0,
|
||||||
|
videos = tonumber(counts.video_count) or 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
|
||||||
local function current_messaging_device(source)
|
local function current_messaging_device(source)
|
||||||
local session, error_response = SkyPhone.RequireSession(source)
|
local session, error_response = SkyPhone.RequireSession(source)
|
||||||
if not session then
|
if not session then
|
||||||
@@ -676,6 +763,7 @@ RegisterNetEvent("sky_phone:media:complete-upload", function(data)
|
|||||||
id = media_id,
|
id = media_id,
|
||||||
url = verified.url,
|
url = verified.url,
|
||||||
mediaType = state.media_type,
|
mediaType = state.media_type,
|
||||||
|
favorite = false,
|
||||||
createdAt = os.time() * 1000,
|
createdAt = os.time() * 1000,
|
||||||
})
|
})
|
||||||
end)
|
end)
|
||||||
@@ -708,6 +796,43 @@ RegisterNetEvent("sky_phone:media:fail-upload", function(data)
|
|||||||
upload_result(src, state.correlation_id, false, error_code)
|
upload_result(src, state.correlation_id, false, error_code)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
local function delete_owned_media(src, owner, media_id)
|
||||||
|
local condition, params = owner_condition(owner)
|
||||||
|
local query_params = { media_id }
|
||||||
|
for _, value in ipairs(params) do
|
||||||
|
query_params[#query_params + 1] = value
|
||||||
|
end
|
||||||
|
local rows = Bridge.Database.Query(([[
|
||||||
|
SELECT `id`, `remote_id`, `origin` FROM `sky_phone_media`
|
||||||
|
WHERE `id` = ? AND %s AND `media_type` IN ('photo', 'video') LIMIT 1
|
||||||
|
]]):format(condition), query_params)
|
||||||
|
local row = rows[1]
|
||||||
|
if not row then
|
||||||
|
return false, "not_found"
|
||||||
|
end
|
||||||
|
local delete_key = row.origin == "phone_upload" and row.remote_id or ("import:%s"):format(media_id)
|
||||||
|
if pending_deletes[delete_key] then
|
||||||
|
return false, "operation_in_progress"
|
||||||
|
end
|
||||||
|
pending_deletes[delete_key] = src
|
||||||
|
if row.origin == "phone_upload" then
|
||||||
|
local references = Bridge.Database.Query(
|
||||||
|
"SELECT COUNT(*) AS `count` FROM `sky_phone_media` WHERE `remote_id` = ?",
|
||||||
|
{ row.remote_id }
|
||||||
|
)
|
||||||
|
if (tonumber(references[1] and references[1].count) or 0) <= 1 then
|
||||||
|
local deleted, delete_error = delete_remote_file(row.remote_id)
|
||||||
|
if not deleted then
|
||||||
|
pending_deletes[delete_key] = nil
|
||||||
|
return false, delete_error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
Bridge.Database.Query(("DELETE FROM `sky_phone_media` WHERE `id` = ? AND %s"):format(condition), query_params)
|
||||||
|
pending_deletes[delete_key] = nil
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
RegisterNetEvent("sky_phone:media:delete", function(data)
|
RegisterNetEvent("sky_phone:media:delete", function(data)
|
||||||
local src = source
|
local src = source
|
||||||
data = type(data) == "table" and data or {}
|
data = type(data) == "table" and data or {}
|
||||||
@@ -726,43 +851,54 @@ RegisterNetEvent("sky_phone:media:delete", function(data)
|
|||||||
delete_result(src, correlation_id, false, error_response.error, media_id)
|
delete_result(src, correlation_id, false, error_response.error, media_id)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local condition, params = owner_condition(owner)
|
local deleted, delete_error = delete_owned_media(src, owner, media_id)
|
||||||
local query_params = { media_id }
|
if not deleted then
|
||||||
for _, value in ipairs(params) do
|
delete_result(src, correlation_id, false, delete_error, media_id)
|
||||||
query_params[#query_params + 1] = value
|
|
||||||
end
|
|
||||||
local rows = Bridge.Database.Query(([[
|
|
||||||
SELECT `id`, `remote_id`, `origin` FROM `sky_phone_media`
|
|
||||||
WHERE `id` = ? AND %s AND `media_type` IN ('photo', 'video') LIMIT 1
|
|
||||||
]]):format(condition), query_params)
|
|
||||||
local row = rows[1]
|
|
||||||
if not row then
|
|
||||||
delete_result(src, correlation_id, false, "not_found", media_id)
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local delete_key = row.origin == "phone_upload" and row.remote_id or ("import:%s"):format(media_id)
|
delete_result(src, correlation_id, true, nil, media_id)
|
||||||
if pending_deletes[delete_key] then
|
end)
|
||||||
delete_result(src, correlation_id, false, "operation_in_progress", media_id)
|
|
||||||
|
RegisterNetEvent("sky_phone:media:delete-many", function(data)
|
||||||
|
local src = source
|
||||||
|
data = type(data) == "table" and data or {}
|
||||||
|
local correlation_id = data.correlationId
|
||||||
|
if type(correlation_id) ~= "string" or #correlation_id > 80 or type(data.ids) ~= "table" then
|
||||||
|
delete_many_result(src, correlation_id, false, "invalid_request", {})
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
pending_deletes[delete_key] = src
|
local media_ids = {}
|
||||||
if row.origin == "phone_upload" then
|
local seen_ids = {}
|
||||||
local references = Bridge.Database.Query(
|
for _, value in ipairs(data.ids) do
|
||||||
"SELECT COUNT(*) AS `count` FROM `sky_phone_media` WHERE `remote_id` = ?",
|
local media_id = tonumber(value)
|
||||||
{ row.remote_id }
|
if media_id and media_id > 0 and media_id == math.floor(media_id) and not seen_ids[media_id] then
|
||||||
)
|
seen_ids[media_id] = true
|
||||||
if (tonumber(references[1] and references[1].count) or 0) <= 1 then
|
media_ids[#media_ids + 1] = media_id
|
||||||
local deleted, delete_error = delete_remote_file(row.remote_id)
|
|
||||||
if not deleted then
|
|
||||||
pending_deletes[delete_key] = nil
|
|
||||||
delete_result(src, correlation_id, false, delete_error, media_id)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
Bridge.Database.Query(("DELETE FROM `sky_phone_media` WHERE `id` = ? AND %s"):format(condition), query_params)
|
if #media_ids < 1 or #media_ids > 50 then
|
||||||
pending_deletes[delete_key] = nil
|
delete_many_result(src, correlation_id, false, "invalid_request", {})
|
||||||
delete_result(src, correlation_id, true, nil, media_id)
|
return
|
||||||
|
end
|
||||||
|
if not SkyPhone.AllowOperation(src, "media_delete_many", 10, 60) then
|
||||||
|
delete_many_result(src, correlation_id, false, "rate_limited", {})
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local owner, error_response = session_owner(src)
|
||||||
|
if not owner then
|
||||||
|
delete_many_result(src, correlation_id, false, error_response.error, {})
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local deleted_ids = {}
|
||||||
|
for _, media_id in ipairs(media_ids) do
|
||||||
|
local deleted, delete_error = delete_owned_media(src, owner, media_id)
|
||||||
|
if not deleted then
|
||||||
|
delete_many_result(src, correlation_id, false, delete_error, deleted_ids)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
deleted_ids[#deleted_ids + 1] = media_id
|
||||||
|
end
|
||||||
|
delete_many_result(src, correlation_id, true, nil, deleted_ids)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
function SkyPhoneMedia.GetDeviceRemoteIds(imei)
|
function SkyPhoneMedia.GetDeviceRemoteIds(imei)
|
||||||
|
|||||||
@@ -351,6 +351,7 @@ local function store_import(owner, media)
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
createdAt = os.time() * 1000,
|
createdAt = os.time() * 1000,
|
||||||
|
favorite = false,
|
||||||
id = media_id,
|
id = media_id,
|
||||||
mediaType = media.mediaType,
|
mediaType = media.mediaType,
|
||||||
url = media.url,
|
url = media.url,
|
||||||
|
|||||||
Reference in New Issue
Block a user