feat(mobile): cache image info (#2899)
This commit is contained in:
parent
bd12076b21
commit
a67d84b7d7
|
|
@ -0,0 +1,5 @@
|
|||
CREATE TABLE `images` (
|
||||
`url` text PRIMARY KEY NOT NULL,
|
||||
`colors` text NOT NULL,
|
||||
`created_at` integer DEFAULT (CURRENT_TIMESTAMP)
|
||||
);
|
||||
|
|
@ -0,0 +1,615 @@
|
|||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "6268a018-0d72-4aa1-9e39-6caab324682e",
|
||||
"prevId": "b19d005b-f43c-474b-99bc-f9aa04dd28dd",
|
||||
"tables": {
|
||||
"collections": {
|
||||
"name": "collections",
|
||||
"columns": {
|
||||
"feed_id": {
|
||||
"name": "feed_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"entry_id": {
|
||||
"name": "entry_id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"view": {
|
||||
"name": "view",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"entries": {
|
||||
"name": "entries",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"title": {
|
||||
"name": "title",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"url": {
|
||||
"name": "url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"content": {
|
||||
"name": "content",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"guid": {
|
||||
"name": "guid",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"author": {
|
||||
"name": "author",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"author_url": {
|
||||
"name": "author_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"author_avatar": {
|
||||
"name": "author_avatar",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"inserted_at": {
|
||||
"name": "inserted_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"published_at": {
|
||||
"name": "published_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"media": {
|
||||
"name": "media",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"categories": {
|
||||
"name": "categories",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"attachments": {
|
||||
"name": "attachments",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"extra": {
|
||||
"name": "extra",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"language": {
|
||||
"name": "language",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"feed_id": {
|
||||
"name": "feed_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"inbox_handle": {
|
||||
"name": "inbox_handle",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"read": {
|
||||
"name": "read",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"feeds": {
|
||||
"name": "feeds",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"title": {
|
||||
"name": "title",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"url": {
|
||||
"name": "url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"image": {
|
||||
"name": "image",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"error_at": {
|
||||
"name": "error_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"site_url": {
|
||||
"name": "site_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"owner_user_id": {
|
||||
"name": "owner_user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"error_message": {
|
||||
"name": "error_message",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"images": {
|
||||
"name": "images",
|
||||
"columns": {
|
||||
"url": {
|
||||
"name": "url",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"colors": {
|
||||
"name": "colors",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "(CURRENT_TIMESTAMP)"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"inboxes": {
|
||||
"name": "inboxes",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"title": {
|
||||
"name": "title",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"lists": {
|
||||
"name": "lists",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"title": {
|
||||
"name": "title",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"feed_ids": {
|
||||
"name": "feed_ids",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"view": {
|
||||
"name": "view",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"image": {
|
||||
"name": "image",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"fee": {
|
||||
"name": "fee",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"owner_user_id": {
|
||||
"name": "owner_user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"entry_ids": {
|
||||
"name": "entry_ids",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"subscriptions": {
|
||||
"name": "subscriptions",
|
||||
"columns": {
|
||||
"feed_id": {
|
||||
"name": "feed_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"list_id": {
|
||||
"name": "list_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"inbox_id": {
|
||||
"name": "inbox_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"view": {
|
||||
"name": "view",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"is_private": {
|
||||
"name": "is_private",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"title": {
|
||||
"name": "title",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"category": {
|
||||
"name": "category",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"summaries": {
|
||||
"name": "summaries",
|
||||
"columns": {
|
||||
"entry_id": {
|
||||
"name": "entry_id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"summary": {
|
||||
"name": "summary",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"language": {
|
||||
"name": "language",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"unq": {
|
||||
"name": "unq",
|
||||
"columns": ["entry_id", "language"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"unread": {
|
||||
"name": "unread",
|
||||
"columns": {
|
||||
"subscription_id": {
|
||||
"name": "subscription_id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"count": {
|
||||
"name": "count",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"users": {
|
||||
"name": "users",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"handle": {
|
||||
"name": "handle",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"image": {
|
||||
"name": "image",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"is_me": {
|
||||
"name": "is_me",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -78,6 +78,13 @@
|
|||
"when": 1739350709266,
|
||||
"tag": "0010_legal_ben_grimm",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 11,
|
||||
"version": "6",
|
||||
"when": 1740623123535,
|
||||
"tag": "0011_mysterious_stark_industries",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import m0007 from "./0007_curvy_tarantula.sql"
|
|||
import m0008 from "./0008_last_the_santerians.sql"
|
||||
import m0009 from "./0009_lucky_power_man.sql"
|
||||
import m0010 from "./0010_legal_ben_grimm.sql"
|
||||
import m0011 from "./0011_mysterious_stark_industries.sql"
|
||||
import journal from "./meta/_journal.json"
|
||||
|
||||
export default {
|
||||
|
|
@ -27,5 +28,6 @@ export default {
|
|||
m0008,
|
||||
m0009,
|
||||
m0010,
|
||||
m0011,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import type { FeedViewType } from "@follow/constants"
|
||||
import { sql } from "drizzle-orm"
|
||||
import { integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core"
|
||||
|
||||
import type { AttachmentsModel, ExtraModel, MediaModel } from "./types"
|
||||
import type { AttachmentsModel, ExtraModel, ImageColorsResult, MediaModel } from "./types"
|
||||
|
||||
export const feedsTable = sqliteTable("feeds", {
|
||||
id: text("id").primaryKey(),
|
||||
|
|
@ -104,3 +105,9 @@ export const summariesTable = sqliteTable(
|
|||
unq: uniqueIndex("unq").on(table.entryId, table.language),
|
||||
}),
|
||||
)
|
||||
|
||||
export const imagesTable = sqliteTable("images", (t) => ({
|
||||
url: t.text("url").notNull().primaryKey(),
|
||||
colors: t.text("colors", { mode: "json" }).$type<ImageColorsResult>().notNull(),
|
||||
createdAt: t.integer("created_at", { mode: "timestamp" }).default(sql`(CURRENT_TIMESTAMP)`),
|
||||
}))
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import type {
|
|||
collectionsTable,
|
||||
entriesTable,
|
||||
feedsTable,
|
||||
imagesTable,
|
||||
inboxesTable,
|
||||
listsTable,
|
||||
subscriptionsTable,
|
||||
|
|
@ -27,6 +28,9 @@ export type EntrySchema = typeof entriesTable.$inferSelect
|
|||
export type CollectionSchema = typeof collectionsTable.$inferSelect
|
||||
|
||||
export type SummarySchema = typeof summariesTable.$inferSelect
|
||||
|
||||
export type ImageSchema = typeof imagesTable.$inferInsert
|
||||
|
||||
export type MediaModel = {
|
||||
url: string
|
||||
type: "photo" | "video"
|
||||
|
|
@ -51,3 +55,5 @@ export type ExtraModel = {
|
|||
content_html?: string
|
||||
}[]
|
||||
}
|
||||
|
||||
export { ImageColorsResult } from "react-native-image-colors"
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { Pressable, Text, View } from "react-native"
|
|||
import Animated, { SlideInDown, SlideOutDown } from "react-native-reanimated"
|
||||
|
||||
import { useActiveTrack } from "@/src/lib/player"
|
||||
import { usePrefetchImageColors } from "@/src/store/image/hooks"
|
||||
|
||||
import { PlayPauseButton, SeekButton } from "./control"
|
||||
|
||||
|
|
@ -13,6 +14,9 @@ const allowedRoutes = new Set(["/", "/subscriptions", "/player"])
|
|||
export function PlayerTabBar({ className }: { className?: string }) {
|
||||
const activeTrack = useActiveTrack()
|
||||
const pathname = usePathname()
|
||||
|
||||
usePrefetchImageColors(activeTrack?.artwork)
|
||||
|
||||
if (!activeTrack || !allowedRoutes.has(pathname)) {
|
||||
return null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
import { getLuminance, shadeColor } from "@follow/utils"
|
||||
import { useMemo } from "react"
|
||||
|
||||
import { useImageColors } from "@/src/store/image/hooks"
|
||||
|
||||
const defaultBackgroundColor = "#000000"
|
||||
|
||||
export function useCoverGradient(url?: string) {
|
||||
const imageColors = useImageColors(url)
|
||||
|
||||
const backgroundColor = useMemo(() => {
|
||||
if (imageColors?.platform === "ios") {
|
||||
return imageColors.background
|
||||
} else if (imageColors?.platform === "android") {
|
||||
return imageColors.average
|
||||
}
|
||||
return defaultBackgroundColor
|
||||
}, [imageColors])
|
||||
|
||||
const isGradientLight = useMemo(() => {
|
||||
return getLuminance(backgroundColor) > 0.5
|
||||
}, [backgroundColor])
|
||||
|
||||
const gradientColors = useMemo(() => {
|
||||
return [backgroundColor, shadeColor(backgroundColor, -50)] as const
|
||||
}, [backgroundColor])
|
||||
|
||||
return { isGradientLight, gradientColors }
|
||||
}
|
||||
|
|
@ -1,63 +1,47 @@
|
|||
import { cn, getLuminance } from "@follow/utils"
|
||||
import { LinearGradient } from "expo-linear-gradient"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useMemo } from "react"
|
||||
import { StyleSheet, Text, View } from "react-native"
|
||||
import ImageColors from "react-native-image-colors"
|
||||
import type { SharedValue } from "react-native-reanimated"
|
||||
import ReAnimated, { FadeIn, FadeOut, interpolate, useAnimatedStyle } from "react-native-reanimated"
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context"
|
||||
|
||||
import { UserAvatar } from "@/src/components/ui/avatar/UserAvatar"
|
||||
import { useImageColors, usePrefetchImageColors } from "@/src/store/image/hooks"
|
||||
import { useWhoami } from "@/src/store/user/hooks"
|
||||
import { accentColor } from "@/src/theme/colors"
|
||||
|
||||
const defaultGradientColors = ["#000", "#100", "#200"]
|
||||
|
||||
export const UserHeaderBanner = ({ scrollY }: { scrollY: SharedValue<number> }) => {
|
||||
const whoami = useWhoami()
|
||||
usePrefetchImageColors(whoami?.image)
|
||||
const insets = useSafeAreaInsets()
|
||||
|
||||
const MAX_PULL = 100
|
||||
const SCALE_FACTOR = 1.8
|
||||
|
||||
const [gradientColors, setGradientColors] = useState<string[]>()
|
||||
const [gradientLight, setGradientLight] = useState<boolean>(false)
|
||||
|
||||
useEffect(() => {
|
||||
const extractColors = async () => {
|
||||
if (!whoami?.image) return
|
||||
|
||||
try {
|
||||
const result = await ImageColors.getColors(whoami.image, {
|
||||
fallback: accentColor,
|
||||
cache: true,
|
||||
})
|
||||
|
||||
if (result.platform === "web") return
|
||||
if (result.platform === "android") {
|
||||
setGradientColors([
|
||||
result.dominant,
|
||||
result.average || result.vibrant,
|
||||
result.vibrant || result.dominant,
|
||||
])
|
||||
|
||||
const dominantLuminance = getLuminance(result.dominant)
|
||||
const isLight = dominantLuminance > 0.5
|
||||
setGradientLight(isLight)
|
||||
} else {
|
||||
const dominantLuminance = getLuminance(result.primary)
|
||||
const isLight = dominantLuminance > 0.5
|
||||
|
||||
setGradientLight(isLight)
|
||||
setGradientColors([result.primary, result.secondary, result.background])
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Failed to extract colors:", error)
|
||||
}
|
||||
const imageColors = useImageColors(whoami?.image)
|
||||
const gradientColors = useMemo(() => {
|
||||
if (!imageColors || imageColors.platform === "web") return defaultGradientColors
|
||||
if (imageColors.platform === "android") {
|
||||
return [
|
||||
imageColors.dominant,
|
||||
imageColors.average || imageColors.vibrant,
|
||||
imageColors.vibrant || imageColors.dominant,
|
||||
]
|
||||
}
|
||||
return [imageColors.primary, imageColors.secondary, imageColors.background]
|
||||
}, [imageColors])
|
||||
|
||||
const gradientLight = useMemo(() => {
|
||||
if (!imageColors) return false
|
||||
if (imageColors.platform === "web") return false
|
||||
const dominantLuminance = getLuminance(
|
||||
imageColors.platform === "android" ? imageColors.dominant : imageColors.primary,
|
||||
)
|
||||
return dominantLuminance > 0.5
|
||||
}, [imageColors])
|
||||
|
||||
extractColors()
|
||||
}, [whoami?.image])
|
||||
const styles = useAnimatedStyle(() => {
|
||||
const scaleValue = interpolate(scrollY.value, [-MAX_PULL, 0], [SCALE_FACTOR, 1], {
|
||||
extrapolateLeft: "extend",
|
||||
|
|
|
|||
|
|
@ -1,18 +1,17 @@
|
|||
import { cn, getLuminance, shadeColor } from "@follow/utils"
|
||||
import { cn } from "@follow/utils"
|
||||
import { Image } from "expo-image"
|
||||
import { LinearGradient } from "expo-linear-gradient"
|
||||
import { router } from "expo-router"
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { useEffect, useMemo } from "react"
|
||||
import { StyleSheet, Text, View } from "react-native"
|
||||
import ImageColors from "react-native-image-colors"
|
||||
import Reanimated, { useAnimatedStyle, useSharedValue, withSpring } from "react-native-reanimated"
|
||||
import { SheetScreen } from "react-native-sheet-transitions"
|
||||
|
||||
import { useActiveTrack, useIsPlaying } from "../lib/player"
|
||||
import { PlayerScreenContext } from "../modules/player/context"
|
||||
import { ControlGroup, ProgressBar, VolumeBar } from "../modules/player/control"
|
||||
|
||||
const defaultBackgroundColor = "#000000"
|
||||
import { useCoverGradient } from "../modules/player/hooks"
|
||||
import { usePrefetchImageColors } from "../store/image/hooks"
|
||||
|
||||
function CoverArt({ cover }: { cover?: string }) {
|
||||
const scale = useSharedValue(1)
|
||||
|
|
@ -37,42 +36,15 @@ function CoverArt({ cover }: { cover?: string }) {
|
|||
|
||||
export default function PlaterScreen() {
|
||||
const activeTrack = useActiveTrack()
|
||||
usePrefetchImageColors(activeTrack?.artwork)
|
||||
|
||||
const [backgroundColor, setBackgroundColor] = useState(defaultBackgroundColor)
|
||||
const [isGradientLight, setIsGradientLight] = useState(false)
|
||||
const { gradientColors, isGradientLight } = useCoverGradient(activeTrack?.artwork)
|
||||
|
||||
const playerScreenContextValue = useMemo(
|
||||
() => ({ isBackgroundLight: isGradientLight }),
|
||||
[isGradientLight],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
async function extractColors() {
|
||||
if (!activeTrack?.artwork) {
|
||||
return
|
||||
}
|
||||
|
||||
const result = await ImageColors.getColors(activeTrack.artwork, {
|
||||
fallback: defaultBackgroundColor,
|
||||
cache: true,
|
||||
})
|
||||
|
||||
if (result.platform === "web") {
|
||||
return
|
||||
}
|
||||
|
||||
if (result.platform === "ios") {
|
||||
setIsGradientLight(getLuminance(result.background) > 0.5)
|
||||
setBackgroundColor(result.background)
|
||||
} else {
|
||||
setIsGradientLight(getLuminance(result.dominant) > 0.5)
|
||||
setBackgroundColor(result.average)
|
||||
}
|
||||
}
|
||||
|
||||
extractColors()
|
||||
}, [activeTrack?.artwork])
|
||||
|
||||
if (!activeTrack) {
|
||||
return null
|
||||
}
|
||||
|
|
@ -83,7 +55,7 @@ export default function PlaterScreen() {
|
|||
<View className="flex-1 px-[1000] p-safe">
|
||||
<LinearGradient
|
||||
style={StyleSheet.absoluteFill}
|
||||
colors={[backgroundColor, shadeColor(backgroundColor, -50)]}
|
||||
colors={gradientColors}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 0 }}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
import { imagesTable } from "@/src/database/schemas"
|
||||
import type { ImageSchema } from "@/src/database/schemas/types"
|
||||
|
||||
import { db } from "../database"
|
||||
import { imageActions } from "../store/image/store"
|
||||
import type { Hydratable, Resetable } from "./internal/base"
|
||||
import { conflictUpdateAllExcept } from "./internal/utils"
|
||||
|
||||
class ImageServiceStatic implements Hydratable, Resetable {
|
||||
async reset() {
|
||||
await db.delete(imagesTable).execute()
|
||||
}
|
||||
async hydrate() {
|
||||
const images = await db.query.imagesTable.findMany()
|
||||
imageActions.upsertManyInSession(images)
|
||||
}
|
||||
|
||||
async upsertMany(imageColors: ImageSchema[]) {
|
||||
if (imageColors.length === 0) return
|
||||
await db
|
||||
.insert(imagesTable)
|
||||
.values(imageColors)
|
||||
.onConflictDoUpdate({
|
||||
target: [imagesTable.url],
|
||||
set: conflictUpdateAllExcept(imagesTable, ["url"]),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const ImagesService = new ImageServiceStatic()
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { CollectionService } from "./collection"
|
||||
import { EntryService } from "./entry"
|
||||
import { FeedService } from "./feed"
|
||||
import { ImagesService } from "./image"
|
||||
import { InboxService } from "./inbox"
|
||||
import type { Hydratable } from "./internal/base"
|
||||
import { ListService } from "./list"
|
||||
|
|
@ -17,6 +18,7 @@ const hydrates: Hydratable[] = [
|
|||
UserService,
|
||||
EntryService,
|
||||
CollectionService,
|
||||
ImagesService,
|
||||
]
|
||||
|
||||
export const hydrateDatabaseToStore = async () => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
import { useImagesStore } from "./store"
|
||||
|
||||
const get = useImagesStore.getState
|
||||
|
||||
export const getImageInfo = (url: string) => {
|
||||
return get().images[url]
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import { useQuery } from "@tanstack/react-query"
|
||||
import { useCallback } from "react"
|
||||
|
||||
import { imageSyncService, useImagesStore } from "./store"
|
||||
|
||||
export const usePrefetchImageColors = (url?: string | null) => {
|
||||
useQuery({
|
||||
queryKey: ["image", "colors", url],
|
||||
queryFn: () => imageSyncService.getColors(url),
|
||||
enabled: !!url,
|
||||
})
|
||||
}
|
||||
|
||||
export const useImageColors = (url?: string | null) => {
|
||||
return useImagesStore(
|
||||
useCallback(
|
||||
(state) => {
|
||||
if (!url) {
|
||||
return
|
||||
}
|
||||
return state.images[url]?.colors
|
||||
},
|
||||
[url],
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
import ImageColors from "react-native-image-colors"
|
||||
|
||||
import type { ImageSchema } from "@/src/database/schemas/types"
|
||||
import { ImagesService } from "@/src/services/image"
|
||||
|
||||
import { createImmerSetter, createTransaction, createZustandStore } from "../internal/helper"
|
||||
import { getImageInfo } from "./getters"
|
||||
|
||||
export type ImageModel = ImageSchema
|
||||
type ImageStore = {
|
||||
images: Record<string, ImageModel>
|
||||
}
|
||||
|
||||
export const useImagesStore = createZustandStore<ImageStore>("images")(() => ({
|
||||
images: {},
|
||||
}))
|
||||
|
||||
const immerSet = createImmerSetter(useImagesStore)
|
||||
|
||||
class ImageSyncService {
|
||||
async getColors(url?: string | null) {
|
||||
if (!url) {
|
||||
return
|
||||
}
|
||||
const existing = getImageInfo(url)?.colors
|
||||
if (existing) {
|
||||
return existing
|
||||
}
|
||||
|
||||
const result = await ImageColors.getColors(url, { cache: true })
|
||||
await imageActions.upsertMany([{ url, colors: result }])
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
class ImageActions {
|
||||
upsertManyInSession(images: ImageModel[]) {
|
||||
immerSet((state) => {
|
||||
for (const image of images) {
|
||||
state.images[image.url] = image
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async upsertMany(images: ImageModel[]) {
|
||||
const tx = createTransaction()
|
||||
tx.store(() => this.upsertManyInSession(images))
|
||||
tx.persist(() => ImagesService.upsertMany(images))
|
||||
await tx.run()
|
||||
}
|
||||
}
|
||||
|
||||
export const imageSyncService = new ImageSyncService()
|
||||
export const imageActions = new ImageActions()
|
||||
Loading…
Reference in New Issue