This commit is contained in:
@@ -255,6 +255,22 @@
|
|||||||
- backend debug broadcasts
|
- backend debug broadcasts
|
||||||
|
|
||||||
## Recent Change Log
|
## Recent Change Log
|
||||||
|
- Date: `2026-03-16`
|
||||||
|
- What changed:
|
||||||
|
- Search enrichment now runs across the full result set sequentially instead of only enriching a capped top subset in parallel.
|
||||||
|
- Gemini Vision evaluation now runs candidate-by-candidate with retries and delay, and fallback-only / clearly negative items are excluded from final output.
|
||||||
|
- Result modal now uses YouTube embed only for Google Video and falls back to source preview video or thumbnail for Envato / Artgrid, avoiding iframe refusal for blocked providers.
|
||||||
|
- When `GEMINI_API_KEY` is absent, the API still returns ranked fallback results so local smoke tests and non-Gemini environments stay usable.
|
||||||
|
- Why it changed:
|
||||||
|
- The user preferred reliability over speed and wanted all candidates processed thoroughly.
|
||||||
|
- Negative or irrelevant Gemini outcomes were still leaking into the final result set.
|
||||||
|
- Envato iframe embeds were being blocked, and Google Video modal opening was broken by missing frontend YouTube ID extraction.
|
||||||
|
- How it was verified:
|
||||||
|
- code inspection of sequential search/enrichment/evaluation flow
|
||||||
|
- What is still risky or incomplete:
|
||||||
|
- Fully sequential Gemini and enrichment processing increases search latency noticeably.
|
||||||
|
- Some Envato / Artgrid items may still only have thumbnails if public preview URLs are not exposed in source metadata.
|
||||||
|
|
||||||
- Date: `2026-03-16`
|
- Date: `2026-03-16`
|
||||||
- What changed:
|
- What changed:
|
||||||
- Result modal layout was rebuilt to match a top `16:9` embedded viewer with bottom-left full AI note and bottom-right action panel.
|
- Result modal layout was rebuilt to match a top `16:9` embedded viewer with bottom-left full AI note and bottom-right action panel.
|
||||||
|
|||||||
+15
-4
@@ -337,14 +337,25 @@ func (a *App) searchMedia(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
if geminiErr != nil && len(recommended) == 0 {
|
if geminiErr != nil && len(recommended) == 0 {
|
||||||
warning := geminiErr.Error()
|
warning := geminiErr.Error()
|
||||||
fallback := services.BuildFallbackRecommendations(scored, 20, "")
|
if strings.Contains(warning, "gemini api key is not configured") {
|
||||||
a.debug("search fallback summary", summarizeRecommendationResults(fallback, time.Since(started), warning))
|
fallback := services.BuildFallbackRecommendations(scored, 20, "")
|
||||||
a.Hub.Broadcast("progress", gin.H{"type": "search", "status": "Gemini Vision fallback to ranked results", "progress": 90, "message": warning})
|
a.debug("search fallback summary", summarizeRecommendationResults(fallback, time.Since(started), warning))
|
||||||
c.JSON(http.StatusOK, gin.H{"results": fallback, "warning": warning, "queries": queryVariants})
|
c.JSON(http.StatusOK, gin.H{"results": fallback, "warning": warning, "queries": queryVariants})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
a.debug("search fallback summary", summarizeRecommendationResults([]services.AIRecommendation{}, time.Since(started), warning))
|
||||||
|
a.Hub.Broadcast("progress", gin.H{"type": "search", "status": "Gemini Vision returned no usable results", "progress": 90, "message": warning})
|
||||||
|
c.JSON(http.StatusOK, gin.H{"results": []services.AIRecommendation{}, "warning": warning, "queries": queryVariants})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
merged := services.MergeRecommendations(recommended, scored, 20)
|
merged := services.MergeRecommendations(recommended, scored, 20)
|
||||||
|
if len(merged) == 0 && len(recommended) > 0 {
|
||||||
|
warning := "Gemini가 대부분의 후보를 부정적으로 평가해 표시할 결과가 없습니다."
|
||||||
|
a.debug("search fallback summary", summarizeRecommendationResults([]services.AIRecommendation{}, time.Since(started), warning))
|
||||||
|
c.JSON(http.StatusOK, gin.H{"results": []services.AIRecommendation{}, "warning": warning, "queries": queryVariants})
|
||||||
|
return
|
||||||
|
}
|
||||||
merged = services.RandomizeTopRecommendations(merged, 8)
|
merged = services.RandomizeTopRecommendations(merged, 8)
|
||||||
warning := ""
|
warning := ""
|
||||||
if geminiErr != nil {
|
if geminiErr != nil {
|
||||||
|
|||||||
+3
-15
@@ -12,7 +12,6 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -127,26 +126,15 @@ func (s *SearchService) SearchMedia(queries []string, enabledPlatforms map[strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *SearchService) EnrichResults(results []SearchResult) []SearchResult {
|
func (s *SearchService) EnrichResults(results []SearchResult) []SearchResult {
|
||||||
limit := minInt(len(results), 18)
|
if len(results) == 0 {
|
||||||
if limit == 0 {
|
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
enriched := make([]SearchResult, len(results))
|
enriched := make([]SearchResult, len(results))
|
||||||
copy(enriched, results)
|
copy(enriched, results)
|
||||||
|
for idx := range enriched {
|
||||||
var wg sync.WaitGroup
|
enriched[idx] = s.enrichResult(enriched[idx])
|
||||||
sem := make(chan struct{}, 4)
|
|
||||||
for idx := 0; idx < limit; idx++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(i int) {
|
|
||||||
defer wg.Done()
|
|
||||||
sem <- struct{}{}
|
|
||||||
defer func() { <-sem }()
|
|
||||||
enriched[i] = s.enrichResult(enriched[i])
|
|
||||||
}(idx)
|
|
||||||
}
|
}
|
||||||
wg.Wait()
|
|
||||||
return enriched
|
return enriched
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -252,10 +252,6 @@ User query: ` + query,
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(recommendations) == 0 {
|
|
||||||
recommendations = BuildFallbackRecommendations(candidates, 8, "Gemini Vision 평가를 받지 못해 키워드 기준으로 보강된 결과입니다.")
|
|
||||||
}
|
|
||||||
|
|
||||||
return recommendations, nil
|
return recommendations, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+35
-98
@@ -5,7 +5,6 @@ import (
|
|||||||
"math/rand"
|
"math/rand"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -91,8 +90,6 @@ func GeminiCandidateLimit(total int) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func EvaluateAllCandidatesWithGemini(service *GeminiService, query string, ranked []SearchResult) ([]AIRecommendation, GeminiBatchStats, error) {
|
func EvaluateAllCandidatesWithGemini(service *GeminiService, query string, ranked []SearchResult) ([]AIRecommendation, GeminiBatchStats, error) {
|
||||||
const chunkSize = 8
|
|
||||||
const maxConcurrentBatches = 2
|
|
||||||
if service == nil {
|
if service == nil {
|
||||||
return nil, GeminiBatchStats{}, fmt.Errorf("gemini service is not configured")
|
return nil, GeminiBatchStats{}, fmt.Errorf("gemini service is not configured")
|
||||||
}
|
}
|
||||||
@@ -102,76 +99,28 @@ func EvaluateAllCandidatesWithGemini(service *GeminiService, query string, ranke
|
|||||||
CandidateCap: limit,
|
CandidateCap: limit,
|
||||||
Requested: min(limit, len(ranked)),
|
Requested: min(limit, len(ranked)),
|
||||||
}
|
}
|
||||||
type batchResult struct {
|
stats.Batches = limit
|
||||||
index int
|
if limit == 0 {
|
||||||
recommendations []AIRecommendation
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
batches := make([][]SearchResult, 0, (limit+chunkSize-1)/chunkSize)
|
|
||||||
for start := 0; start < limit; start += chunkSize {
|
|
||||||
end := start + chunkSize
|
|
||||||
if end > limit {
|
|
||||||
end = limit
|
|
||||||
}
|
|
||||||
batches = append(batches, ranked[start:end])
|
|
||||||
}
|
|
||||||
stats.Batches = len(batches)
|
|
||||||
if len(batches) == 0 {
|
|
||||||
return []AIRecommendation{}, stats, nil
|
return []AIRecommendation{}, stats, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
results := make([]batchResult, len(batches))
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
sem := make(chan struct{}, maxConcurrentBatches)
|
|
||||||
for idx, batch := range batches {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(batchIndex int, candidates []SearchResult) {
|
|
||||||
defer wg.Done()
|
|
||||||
sem <- struct{}{}
|
|
||||||
defer func() { <-sem }()
|
|
||||||
recommended, err := service.Recommend(query, candidates)
|
|
||||||
results[batchIndex] = batchResult{
|
|
||||||
index: batchIndex,
|
|
||||||
recommendations: recommended,
|
|
||||||
err: err,
|
|
||||||
}
|
|
||||||
}(idx, batch)
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
merged := make([]AIRecommendation, 0, len(ranked))
|
merged := make([]AIRecommendation, 0, len(ranked))
|
||||||
seen := map[string]bool{}
|
seen := map[string]bool{}
|
||||||
for _, batch := range results {
|
for idx := 0; idx < limit; idx++ {
|
||||||
if batch.err != nil {
|
recommendations, err := recoverGeminiCandidateSequentially(service, query, ranked[idx])
|
||||||
recovered, recoveredErrs := recoverGeminiBatchSequentially(service, query, ranked, batch.index*chunkSize)
|
if err != nil {
|
||||||
if len(recovered) > 0 {
|
|
||||||
stats.SequentialRetried++
|
|
||||||
stats.Succeeded++
|
|
||||||
for _, item := range recovered {
|
|
||||||
if item.Link == "" || seen[item.Link] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[item.Link] = true
|
|
||||||
merged = append(merged, item)
|
|
||||||
}
|
|
||||||
if len(recoveredErrs) > 0 {
|
|
||||||
stats.Failed++
|
|
||||||
for _, recoveredErr := range recoveredErrs {
|
|
||||||
if len(stats.Errors) < 5 {
|
|
||||||
stats.Errors = append(stats.Errors, recoveredErr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
stats.Failed++
|
stats.Failed++
|
||||||
if len(stats.Errors) < 5 {
|
if len(stats.Errors) < 5 {
|
||||||
stats.Errors = append(stats.Errors, batch.err.Error())
|
stats.Errors = append(stats.Errors, err.Error())
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
stats.Succeeded++
|
stats.Succeeded++
|
||||||
for _, item := range batch.recommendations {
|
if len(recommendations) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
stats.SequentialRetried++
|
||||||
|
for _, item := range recommendations {
|
||||||
if item.Link == "" || seen[item.Link] {
|
if item.Link == "" || seen[item.Link] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -185,12 +134,12 @@ func EvaluateAllCandidatesWithGemini(service *GeminiService, query string, ranke
|
|||||||
case len(merged) > 0 && stats.Failed == 0:
|
case len(merged) > 0 && stats.Failed == 0:
|
||||||
return merged, stats, nil
|
return merged, stats, nil
|
||||||
case len(merged) > 0 && stats.Failed > 0:
|
case len(merged) > 0 && stats.Failed > 0:
|
||||||
return merged, stats, fmt.Errorf("gemini vision partially failed on %d of %d batches", stats.Failed, stats.Batches)
|
return merged, stats, fmt.Errorf("gemini vision partially failed on %d of %d candidates", stats.Failed, stats.Batches)
|
||||||
case stats.Failed == stats.Batches:
|
case stats.Failed == stats.Batches:
|
||||||
if len(stats.Errors) > 0 {
|
if len(stats.Errors) > 0 {
|
||||||
return nil, stats, fmt.Errorf("gemini vision failed for all batches: %s", strings.Join(stats.Errors, "; "))
|
return nil, stats, fmt.Errorf("gemini vision failed for all candidates: %s", strings.Join(stats.Errors, "; "))
|
||||||
}
|
}
|
||||||
return nil, stats, fmt.Errorf("gemini vision failed for all batches")
|
return nil, stats, fmt.Errorf("gemini vision failed for all candidates")
|
||||||
default:
|
default:
|
||||||
return nil, stats, fmt.Errorf("gemini vision returned no candidate evaluations")
|
return nil, stats, fmt.Errorf("gemini vision returned no candidate evaluations")
|
||||||
}
|
}
|
||||||
@@ -232,23 +181,21 @@ func RandomizeTopRecommendations(items []AIRecommendation, window int) []AIRecom
|
|||||||
return shuffled
|
return shuffled
|
||||||
}
|
}
|
||||||
|
|
||||||
func recoverGeminiBatchSequentially(service *GeminiService, query string, ranked []SearchResult, startIndex int) ([]AIRecommendation, []string) {
|
func recoverGeminiCandidateSequentially(service *GeminiService, query string, candidate SearchResult) ([]AIRecommendation, error) {
|
||||||
recovered := make([]AIRecommendation, 0, 8)
|
var lastErr error
|
||||||
errs := make([]string, 0, 4)
|
for attempt := 0; attempt < 3; attempt++ {
|
||||||
endIndex := min(startIndex+8, len(ranked))
|
recs, err := service.Recommend(query, []SearchResult{candidate})
|
||||||
for idx := startIndex; idx < endIndex; idx++ {
|
|
||||||
recs, err := service.Recommend(query, []SearchResult{ranked[idx]})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if len(errs) < 4 {
|
lastErr = err
|
||||||
errs = append(errs, err.Error())
|
time.Sleep(450 * time.Millisecond)
|
||||||
}
|
|
||||||
time.Sleep(350 * time.Millisecond)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
recovered = append(recovered, recs...)
|
return recs, nil
|
||||||
time.Sleep(350 * time.Millisecond)
|
|
||||||
}
|
}
|
||||||
return recovered, errs
|
if lastErr == nil {
|
||||||
|
lastErr = fmt.Errorf("gemini vision sequential retry returned no result")
|
||||||
|
}
|
||||||
|
return nil, lastErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func NeedsSupplementalExploration(items []AIRecommendation) bool {
|
func NeedsSupplementalExploration(items []AIRecommendation) bool {
|
||||||
@@ -266,7 +213,7 @@ func NeedsSupplementalExploration(items []AIRecommendation) bool {
|
|||||||
negativeCount++
|
negativeCount++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if recommendedCount >= 3 {
|
if recommendedCount >= 5 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return negativeCount >= max(2, len(items)/2)
|
return negativeCount >= max(2, len(items)/2)
|
||||||
@@ -293,7 +240,7 @@ func MergeRecommendations(recommended []AIRecommendation, ranked []SearchResult,
|
|||||||
seen := map[string]bool{}
|
seen := map[string]bool{}
|
||||||
|
|
||||||
for _, item := range recommended {
|
for _, item := range recommended {
|
||||||
if !item.Recommended {
|
if !item.Recommended || shouldExcludeRecommendation(item) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if item.Link == "" || seen[item.Link] {
|
if item.Link == "" || seen[item.Link] {
|
||||||
@@ -304,29 +251,12 @@ func MergeRecommendations(recommended []AIRecommendation, ranked []SearchResult,
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, item := range recommended {
|
for _, item := range recommended {
|
||||||
if item.Recommended || item.Link == "" || seen[item.Link] || len(merged) >= limit {
|
if item.Recommended || item.Link == "" || seen[item.Link] || len(merged) >= limit || shouldExcludeRecommendation(item) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
seen[item.Link] = true
|
seen[item.Link] = true
|
||||||
merged = append(merged, item)
|
merged = append(merged, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, item := range ranked {
|
|
||||||
if len(merged) >= limit || item.Link == "" || seen[item.Link] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[item.Link] = true
|
|
||||||
merged = append(merged, AIRecommendation{
|
|
||||||
Title: item.Title,
|
|
||||||
Link: item.Link,
|
|
||||||
Snippet: item.Snippet,
|
|
||||||
ThumbnailURL: item.ThumbnailURL,
|
|
||||||
PreviewVideoURL: item.PreviewVideoURL,
|
|
||||||
Source: item.Source,
|
|
||||||
Reason: GeminiFallbackReason,
|
|
||||||
Recommended: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return merged
|
return merged
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,3 +266,10 @@ func max(a, b int) int {
|
|||||||
}
|
}
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func shouldExcludeRecommendation(item AIRecommendation) bool {
|
||||||
|
if strings.Contains(item.Reason, GeminiFallbackReason) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return looksNegativeReason(item.Reason)
|
||||||
|
}
|
||||||
|
|||||||
+71
-1
@@ -43,6 +43,9 @@ const resultModalSource = document.getElementById("resultModalSource");
|
|||||||
const resultModalSnippet = document.getElementById("resultModalSnippet");
|
const resultModalSnippet = document.getElementById("resultModalSnippet");
|
||||||
const resultModalReason = document.getElementById("resultModalReason");
|
const resultModalReason = document.getElementById("resultModalReason");
|
||||||
const resultModalFrame = document.getElementById("resultModalFrame");
|
const resultModalFrame = document.getElementById("resultModalFrame");
|
||||||
|
const resultModalMediaFrame = document.getElementById("resultModalMediaFrame");
|
||||||
|
const resultModalVideo = document.getElementById("resultModalVideo");
|
||||||
|
const resultModalThumbnail = document.getElementById("resultModalThumbnail");
|
||||||
const resultModalOpenExternal = document.getElementById("resultModalOpenExternal");
|
const resultModalOpenExternal = document.getElementById("resultModalOpenExternal");
|
||||||
const resultModalDownload = document.getElementById("resultModalDownload");
|
const resultModalDownload = document.getElementById("resultModalDownload");
|
||||||
const closeResultModal = document.getElementById("closeResultModal");
|
const closeResultModal = document.getElementById("closeResultModal");
|
||||||
@@ -53,6 +56,9 @@ const resultModalReady = Boolean(
|
|||||||
resultModalSnippet &&
|
resultModalSnippet &&
|
||||||
resultModalReason &&
|
resultModalReason &&
|
||||||
resultModalFrame &&
|
resultModalFrame &&
|
||||||
|
resultModalMediaFrame &&
|
||||||
|
resultModalVideo &&
|
||||||
|
resultModalThumbnail &&
|
||||||
resultModalOpenExternal &&
|
resultModalOpenExternal &&
|
||||||
resultModalDownload &&
|
resultModalDownload &&
|
||||||
closeResultModal,
|
closeResultModal,
|
||||||
@@ -157,6 +163,23 @@ function toClock(totalSeconds) {
|
|||||||
return `${hours}:${minutes}:${secs}`;
|
return `${hours}:${minutes}:${secs}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractYouTubeID(link) {
|
||||||
|
if (!link) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const patterns = [
|
||||||
|
/(?:v=|\/shorts\/|\/embed\/)([A-Za-z0-9_-]{11})/,
|
||||||
|
/youtu\.be\/([A-Za-z0-9_-]{11})/,
|
||||||
|
];
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
const match = link.match(pattern);
|
||||||
|
if (match?.[1]) {
|
||||||
|
return match[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
function syncRanges() {
|
function syncRanges() {
|
||||||
let start = cropStart;
|
let start = cropStart;
|
||||||
let end = cropEnd;
|
let end = cropEnd;
|
||||||
@@ -373,7 +396,36 @@ function resetResultModalMedia() {
|
|||||||
if (!resultModalReady) {
|
if (!resultModalReady) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
resultModalVideo.pause();
|
||||||
|
detachVideoSource(resultModalVideo);
|
||||||
resultModalFrame.src = "about:blank";
|
resultModalFrame.src = "about:blank";
|
||||||
|
resultModalThumbnail.removeAttribute("src");
|
||||||
|
setHidden(resultModalFrame, true, "");
|
||||||
|
setHidden(resultModalVideo, true, "");
|
||||||
|
setHidden(resultModalThumbnail, true, "");
|
||||||
|
resultModalMediaFrame.style.aspectRatio = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function showResultModalFrame(src) {
|
||||||
|
if (!src) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resultModalFrame.src = src;
|
||||||
|
setHidden(resultModalFrame, false, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function showResultModalVideo(src) {
|
||||||
|
if (!src) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
attachVideoSource(resultModalVideo, src);
|
||||||
|
setHidden(resultModalVideo, false, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function showResultModalThumbnail(src, alt) {
|
||||||
|
resultModalThumbnail.src = src || PREVIEW_PLACEHOLDER;
|
||||||
|
resultModalThumbnail.alt = alt || "";
|
||||||
|
setHidden(resultModalThumbnail, false, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderResults(results) {
|
function renderResults(results) {
|
||||||
@@ -452,7 +504,13 @@ function openResultModal(item) {
|
|||||||
const canDirectDownload = item.source === "Google Video" && item.link;
|
const canDirectDownload = item.source === "Google Video" && item.link;
|
||||||
resultModalDownload.classList.toggle("hidden", !canDirectDownload);
|
resultModalDownload.classList.toggle("hidden", !canDirectDownload);
|
||||||
resetResultModalMedia();
|
resetResultModalMedia();
|
||||||
resultModalFrame.src = buildResultModalEmbedURL(item);
|
if (item.source === "Google Video") {
|
||||||
|
showResultModalFrame(buildResultModalEmbedURL(item));
|
||||||
|
} else if (item.previewVideoUrl) {
|
||||||
|
showResultModalVideo(item.previewVideoUrl);
|
||||||
|
} else {
|
||||||
|
showResultModalThumbnail(item.thumbnailUrl, item.title || "");
|
||||||
|
}
|
||||||
showModal(resultModal);
|
showModal(resultModal);
|
||||||
logEvent("result:modal:open", { title: item.title, source: item.source, link: item.link });
|
logEvent("result:modal:open", { title: item.title, source: item.source, link: item.link });
|
||||||
}
|
}
|
||||||
@@ -683,6 +741,18 @@ previewThumbnail.addEventListener("load", () => {
|
|||||||
previewMediaFrame.style.aspectRatio = `${previewThumbnail.naturalWidth} / ${previewThumbnail.naturalHeight}`;
|
previewMediaFrame.style.aspectRatio = `${previewThumbnail.naturalWidth} / ${previewThumbnail.naturalHeight}`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if (resultModalReady) {
|
||||||
|
resultModalVideo.addEventListener("loadedmetadata", () => {
|
||||||
|
if (resultModalVideo.videoWidth > 0 && resultModalVideo.videoHeight > 0) {
|
||||||
|
resultModalMediaFrame.style.aspectRatio = `${resultModalVideo.videoWidth} / ${resultModalVideo.videoHeight}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
resultModalThumbnail.addEventListener("load", () => {
|
||||||
|
if (resultModalThumbnail.naturalWidth > 0 && resultModalThumbnail.naturalHeight > 0) {
|
||||||
|
resultModalMediaFrame.style.aspectRatio = `${resultModalThumbnail.naturalWidth} / ${resultModalThumbnail.naturalHeight}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
for (const button of platformToggles) {
|
for (const button of platformToggles) {
|
||||||
button.addEventListener("click", () => {
|
button.addEventListener("click", () => {
|
||||||
const platform = button.dataset.platformToggle;
|
const platform = button.dataset.platformToggle;
|
||||||
|
|||||||
+5
-3
@@ -162,8 +162,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="border-b border-white/10 bg-black/40 p-4">
|
<div class="border-b border-white/10 bg-black/40 p-4">
|
||||||
<div class="aspect-video overflow-hidden rounded-2xl border border-white/10 bg-black">
|
<div id="resultModalMediaFrame" class="aspect-video overflow-hidden rounded-2xl border border-white/10 bg-black">
|
||||||
<iframe id="resultModalFrame" class="h-full w-full bg-white" referrerpolicy="no-referrer" allow="autoplay; fullscreen; encrypted-media; picture-in-picture" allowfullscreen></iframe>
|
<iframe id="resultModalFrame" class="hidden h-full w-full bg-white" referrerpolicy="no-referrer" allow="autoplay; fullscreen; encrypted-media; picture-in-picture" allowfullscreen></iframe>
|
||||||
|
<video id="resultModalVideo" class="hidden h-full w-full bg-black object-contain" controls playsinline></video>
|
||||||
|
<img id="resultModalThumbnail" class="hidden h-full w-full object-contain" alt="" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid gap-5 px-5 py-5 lg:grid-cols-[1.6fr_0.8fr]">
|
<div class="grid gap-5 px-5 py-5 lg:grid-cols-[1.6fr_0.8fr]">
|
||||||
@@ -200,6 +202,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="/app.js?v=20260316c" defer></script>
|
<script src="/app.js?v=20260316d" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user