mirror of
https://github.com/julyx10/lap.git
synced 2026-10-02 10:05:16 +08:00
feat: Implement a custom thumb:// URI scheme to serve raw thumbnail images directly from the database, replacing base64 data URLs.
This commit is contained in:
Generated
+1
@@ -13,6 +13,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"dirs 5.0.1",
|
||||
"ffmpeg-next",
|
||||
"http",
|
||||
"image",
|
||||
"imagesize",
|
||||
"kamadak-exif",
|
||||
|
||||
@@ -47,6 +47,7 @@ dirs = "5.0.1"
|
||||
walkdir = "2.5.0"
|
||||
trash = "3.0.0"
|
||||
base64 = "0.22.1"
|
||||
http = "1"
|
||||
image = "0.25.5"
|
||||
imagesize = "0.13.0"
|
||||
tokio = { version = "1.44.1", features = ["full"] }
|
||||
|
||||
@@ -47,6 +47,43 @@ fn main() {
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_process::init())
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.register_uri_scheme_protocol("thumb", |_ctx, request| {
|
||||
// URL format: thumb://localhost/{library_id}/{file_id}
|
||||
// library_id is for browser cache isolation only; file_id is the last segment
|
||||
let path = request.uri().path();
|
||||
let file_id_str = path.rsplit('/').next().unwrap_or("");
|
||||
let file_id: i64 = file_id_str.parse().unwrap_or(0);
|
||||
|
||||
if file_id <= 0 {
|
||||
return http::Response::builder()
|
||||
.status(http::StatusCode::BAD_REQUEST)
|
||||
.header(http::header::CONTENT_TYPE, "text/plain")
|
||||
.body("invalid file_id".as_bytes().to_vec())
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
match t_sqlite::AThumb::fetch_raw(file_id) {
|
||||
Ok(Some(data)) => {
|
||||
// Detect format: PNG starts with 0x89504E47, else assume JPEG
|
||||
let mime = if data.starts_with(&[0x89, 0x50, 0x4E, 0x47]) {
|
||||
"image/png"
|
||||
} else {
|
||||
"image/jpeg"
|
||||
};
|
||||
http::Response::builder()
|
||||
.status(http::StatusCode::OK)
|
||||
.header(http::header::CONTENT_TYPE, mime)
|
||||
.header(http::header::CACHE_CONTROL, "max-age=31536000, immutable")
|
||||
.body(data)
|
||||
.unwrap()
|
||||
}
|
||||
_ => http::Response::builder()
|
||||
.status(http::StatusCode::NOT_FOUND)
|
||||
.header(http::header::CONTENT_TYPE, "text/plain")
|
||||
.body("thumbnail not found".as_bytes().to_vec())
|
||||
.unwrap(),
|
||||
}
|
||||
})
|
||||
.manage(t_ai::AiState(std::sync::Mutex::new(t_ai::AiEngine::new())))
|
||||
.manage(t_face::FaceState(std::sync::Arc::new(
|
||||
std::sync::Mutex::new(t_face::FaceEngine::new()),
|
||||
|
||||
@@ -2217,6 +2217,23 @@ impl AThumb {
|
||||
Self::fetch(file_id)
|
||||
}
|
||||
|
||||
/// fetch raw thumbnail bytes from db (no base64 encoding) for protocol handler
|
||||
pub fn fetch_raw(file_id: i64) -> Result<Option<Vec<u8>>, String> {
|
||||
let conn = open_conn()?;
|
||||
let result = conn
|
||||
.query_row(
|
||||
"SELECT thumb_data FROM athumbs WHERE file_id = ?1 AND error_code = 0",
|
||||
params![file_id],
|
||||
|row| {
|
||||
let data: Option<Vec<u8>> = row.get(0)?;
|
||||
Ok(data)
|
||||
},
|
||||
)
|
||||
.optional()
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(result.flatten())
|
||||
}
|
||||
|
||||
/// delete a thumbnail from db
|
||||
pub fn delete(file_id: i64) -> Result<usize, String> {
|
||||
let conn = open_conn()?;
|
||||
|
||||
@@ -285,14 +285,27 @@ export function formatFolderBreadcrumb(
|
||||
.join(' > ');
|
||||
}
|
||||
|
||||
let _thumbLibraryId = 'default';
|
||||
|
||||
export function setThumbLibraryId(id: string) {
|
||||
_thumbLibraryId = id || 'default';
|
||||
}
|
||||
|
||||
export function getThumbUrl(fileId: number | null | undefined, bustCache = false): string {
|
||||
if (!fileId || fileId <= 0) return '';
|
||||
const base = `thumb://localhost/${_thumbLibraryId}/${fileId}`;
|
||||
return bustCache ? `${base}?t=${Date.now()}` : base;
|
||||
}
|
||||
|
||||
export function getThumbnailDataUrl(
|
||||
thumb: { thumb_data_base64?: string | null; thumb_mime_type?: string | null } | null | undefined,
|
||||
placeholder = ''
|
||||
thumb: { file_id?: number | null; error_code?: number | null } | null | undefined,
|
||||
placeholder = '',
|
||||
bustCache = false
|
||||
): string {
|
||||
if (!thumb?.thumb_data_base64) {
|
||||
if (!thumb || thumb.error_code !== 0 || !thumb.file_id) {
|
||||
return placeholder;
|
||||
}
|
||||
return `data:${thumb.thumb_mime_type || 'image/jpeg'};base64,${thumb.thumb_data_base64}`;
|
||||
return getThumbUrl(thumb.file_id, bustCache) || placeholder;
|
||||
}
|
||||
|
||||
export function getRelativePath(path: string, basePath: string): string {
|
||||
|
||||
@@ -183,7 +183,7 @@ import { VueDraggable } from 'vue-draggable-plus'
|
||||
import { listen, emit as tauriEmit } from '@tauri-apps/api/event';
|
||||
import { config, libConfig } from '@/common/config';
|
||||
import { useUIStore } from '@/stores/uiStore';
|
||||
import { scrollToFolder, formatTimestamp } from '@/common/utils';
|
||||
import { scrollToFolder, formatTimestamp, getThumbUrl } from '@/common/utils';
|
||||
import { getAlbumQueueIndex, getAlbumScanState, getAlbumScanIcon, shouldAnimateAlbumScanIcon } from '@/common/scanStatus';
|
||||
import { getAllAlbums, setDisplayOrder, addAlbum, editAlbum, removeAlbum,
|
||||
fetchFolder, expandFinalFolder, getFileThumb,
|
||||
@@ -340,13 +340,9 @@ const getMoreMenuItems = (album: any) => {
|
||||
// Load cover thumbnail for a single album
|
||||
const loadAlbumCover = async (albumId: number, coverFileId: number | null) => {
|
||||
if (coverFileId) {
|
||||
try {
|
||||
const thumb = await getFileThumb(coverFileId, "", 0, 0, config.settings.thumbnailSize, false);
|
||||
if (thumb && thumb.error_code === 0) {
|
||||
albumCovers.value[albumId] = `data:image/jpeg;base64,${thumb.thumb_data_base64}`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to load cover for album ${albumId}:`, error);
|
||||
const url = getThumbUrl(coverFileId);
|
||||
if (url) {
|
||||
albumCovers.value[albumId] = url;
|
||||
}
|
||||
} else {
|
||||
delete albumCovers.value[albumId];
|
||||
|
||||
@@ -3747,7 +3747,7 @@ const updateThumbForFile = async (file: any) => {
|
||||
const thumb = await getFileThumb(file.id, file.file_path, file.file_type, file.e_orientation || 0, config.settings.thumbnailSize, true);
|
||||
if(thumb) {
|
||||
if(thumb.error_code === 0) {
|
||||
file.thumbnail = getThumbnailDataUrl(thumb, thumbnailPlaceholder);
|
||||
file.thumbnail = getThumbnailDataUrl(thumb, thumbnailPlaceholder, true);
|
||||
} else if(thumb.error_code === 1) {
|
||||
file.thumbnail = thumbnailPlaceholder;
|
||||
}
|
||||
|
||||
@@ -208,6 +208,7 @@ import { listen } from '@tauri-apps/api/event';
|
||||
import { useUIStore } from '@/stores/uiStore';
|
||||
import MessageBox from '@/components/MessageBox.vue';
|
||||
import { getFileThumb, getFileInfo } from '@/common/api';
|
||||
import { getThumbUrl } from '@/common/utils';
|
||||
|
||||
import { IconMore, IconTrash, IconSearch, IconDot, IconClose } from '@/common/icons';
|
||||
import ContextMenu from '@/components/ContextMenu.vue';
|
||||
@@ -423,15 +424,7 @@ async function fetchThumbnailsForIds(ids: number[]) {
|
||||
}
|
||||
|
||||
if (historyItems.value[fileId] && !thumbnails.value[fileId]) {
|
||||
try {
|
||||
const file = historyItems.value[fileId];
|
||||
const thumb = await getFileThumb(file.id, file.file_path, file.file_type || 1, file.e_orientation || 0, config.settings.thumbnailSize, false);
|
||||
if (thumb && thumb.error_code === 0) {
|
||||
thumbnails.value[file.id] = `data:image/jpeg;base64,${thumb.thumb_data_base64}`;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load thumbnail for history item', fileId, e);
|
||||
}
|
||||
thumbnails.value[fileId] = getThumbUrl(fileId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
import { defineStore } from 'pinia';
|
||||
import { getCurrentLibraryState, saveLibraryState, getAppConfig } from '@/common/api';
|
||||
import { setThumbLibraryId } from '@/common/utils';
|
||||
|
||||
export const useLibraryStore = defineStore('libraryStore', {
|
||||
state: () => ({
|
||||
@@ -102,6 +103,7 @@ export const useLibraryStore = defineStore('libraryStore', {
|
||||
const appConfig = await getAppConfig();
|
||||
if (appConfig) {
|
||||
this._libraryId = appConfig.current_library_id;
|
||||
setThumbLibraryId(appConfig.current_library_id);
|
||||
}
|
||||
|
||||
// Load library state from backend
|
||||
|
||||
Reference in New Issue
Block a user