mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-04-20 14:52:13 +00:00
made chages to relevant files to add the requested features
This commit is contained in:
parent
536be36005
commit
b3b28d4abb
4 changed files with 239 additions and 0 deletions
32
add-meta-links.patch
Normal file
32
add-meta-links.patch
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
diff --git a/src/routes/MetaDetails/MetaDetails.js b/src/routes/MetaDetails/MetaDetails.js
|
||||
index 1234567..89abcde 100644
|
||||
--- a/src/routes/MetaDetails/MetaDetails.js
|
||||
+++ b/src/routes/MetaDetails/MetaDetails.js
|
||||
@@ -11,6 +11,7 @@ const useMetaDetails = require('./useMetaDetails');
|
||||
const withCoreSuspender = require('stremio/common/withCoreSuspender');
|
||||
const MetaVideosList = require('./VideosList/VideosList');
|
||||
const MetaStreamsList = require('./StreamsList/StreamsList');
|
||||
+const MetaLinks = require('../../components/MetaLinks').default;
|
||||
|
||||
const MetaDetails = ({ urlParams, queryParams }) => {
|
||||
const metaDetails = useMetaDetails(urlParams);
|
||||
@@ -100,6 +101,19 @@ const MetaDetails = ({ urlParams, queryParams }) => {
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
+ {/* Additional links section */}
|
||||
+ {metaDetails && metaDetails.links && metaDetails.links.length > 0 && (
|
||||
+ <section
|
||||
+ className="meta-links-section"
|
||||
+ aria-label="Additional links"
|
||||
+ style={{ marginTop: 20 }}
|
||||
+ >
|
||||
+ <h3 style={{ marginBottom: 8 }}>Additional links</h3>
|
||||
+ <MetaLinks links={metaDetails.links} meta={metaDetails} />
|
||||
+ </section>
|
||||
+ )}
|
||||
+
|
||||
+
|
||||
{metaDetails && metaDetails.trailers && metaDetails.trailers.length ? (
|
||||
<section className="trailers">
|
||||
<h3>Trailers</h3>
|
||||
114
src/components/MetaLinks.jsx
Normal file
114
src/components/MetaLinks.jsx
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
// src/components/MetaLinks.jsx
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { handleMetaLink, copyToClipboard } from '../utils/linkHandler';
|
||||
|
||||
/**
|
||||
* MetaLinks component
|
||||
* Props:
|
||||
* - links: array of meta.link objects
|
||||
* - meta: the meta object (passed to playInApp as context)
|
||||
*/
|
||||
export default function MetaLinks({ links = [], meta = {} }) {
|
||||
if (!links || !links.length) return null;
|
||||
|
||||
const onPrimaryClick = async (link) => {
|
||||
await handleMetaLink(link, meta);
|
||||
};
|
||||
|
||||
const onCopyClick = async (e, link) => {
|
||||
e.stopPropagation();
|
||||
try {
|
||||
await copyToClipboard(link.url);
|
||||
showTemporaryToast('Link copied');
|
||||
} catch (err) {
|
||||
showTemporaryToast('Copy failed');
|
||||
}
|
||||
};
|
||||
|
||||
const showTemporaryToast = (text) => {
|
||||
const toast = document.createElement('div');
|
||||
toast.innerText = text;
|
||||
toast.style.position = 'fixed';
|
||||
toast.style.bottom = '20px';
|
||||
toast.style.right = '20px';
|
||||
toast.style.padding = '8px 12px';
|
||||
toast.style.background = 'rgba(0,0,0,0.8)';
|
||||
toast.style.color = 'white';
|
||||
toast.style.borderRadius = '6px';
|
||||
toast.style.zIndex = 9999;
|
||||
document.body.appendChild(toast);
|
||||
setTimeout(() => {
|
||||
try { document.body.removeChild(toast); } catch (e) {}
|
||||
}, 1400);
|
||||
};
|
||||
|
||||
const renderLabel = (link) => {
|
||||
if (link.name) return link.name;
|
||||
if (link.type) return link.type;
|
||||
try {
|
||||
return new URL(link.url).hostname;
|
||||
} catch (e) {
|
||||
return link.url;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="meta-links" style={{ display: 'flex', gap: 10, flexWrap: 'wrap', marginTop: 14 }}>
|
||||
{links.map((link, idx) => (
|
||||
<div
|
||||
key={`${link.url}-${idx}`}
|
||||
className="meta-link"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => onPrimaryClick(link)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') onPrimaryClick(link); }}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
padding: '8px 12px',
|
||||
borderRadius: 8,
|
||||
border: '1px solid rgba(0,0,0,0.08)',
|
||||
cursor: 'pointer',
|
||||
minWidth: 140,
|
||||
background: 'var(--card-bg, #fff)',
|
||||
boxShadow: 'var(--card-shadow, 0 1px 2px rgba(0,0,0,0.04))',
|
||||
}}
|
||||
aria-label={`Meta link ${renderLabel(link)}`}
|
||||
>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontSize: 14, fontWeight: 600 }}>{renderLabel(link)}</div>
|
||||
{link.info ? (
|
||||
<div style={{ fontSize: 12, color: 'rgba(0,0,0,0.6)' }}>{link.info}</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`Copy link ${renderLabel(link)}`}
|
||||
onClick={(e) => onCopyClick(e, link)}
|
||||
style={{ border: 'none', background: 'transparent', cursor: 'pointer', fontSize: 16 }}
|
||||
>
|
||||
📋
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`Open ${renderLabel(link)}`}
|
||||
onClick={(e) => { e.stopPropagation(); onPrimaryClick(link); }}
|
||||
style={{ border: 'none', background: 'transparent', cursor: 'pointer', fontSize: 16 }}
|
||||
>
|
||||
↗
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
MetaLinks.propTypes = {
|
||||
links: PropTypes.arrayOf(PropTypes.object),
|
||||
meta: PropTypes.object,
|
||||
};
|
||||
|
|
@ -13,6 +13,8 @@ const useMetaDetails = require('./useMetaDetails');
|
|||
const useSeason = require('./useSeason');
|
||||
const useMetaExtensionTabs = require('./useMetaExtensionTabs');
|
||||
const styles = require('./styles');
|
||||
const MetaLinks = require('../../components/MetaLinks').default;
|
||||
|
||||
|
||||
const MetaDetails = ({ urlParams, queryParams }) => {
|
||||
const { t } = useTranslation();
|
||||
|
|
@ -94,6 +96,8 @@ const MetaDetails = ({ urlParams, queryParams }) => {
|
|||
metaDetails.metaItem.content.content.background.length > 0
|
||||
), [metaPath, metaDetails]);
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className={styles['metadetails-container']}>
|
||||
{
|
||||
|
|
@ -171,6 +175,11 @@ const MetaDetails = ({ urlParams, queryParams }) => {
|
|||
metaId={metaDetails.metaItem.content.content.id}
|
||||
ratingInfo={metaDetails.ratingInfo}
|
||||
/>
|
||||
{/* Render additional meta links if present */}
|
||||
{metaDetails.metaItem?.content?.content?.links?.length > 0 && (
|
||||
<MetaLinks links={metaDetails.metaItem.content.content.links} />
|
||||
)}
|
||||
|
||||
</React.Fragment>
|
||||
}
|
||||
<div className={styles['spacing']} />
|
||||
|
|
|
|||
84
src/utils/linkHandler.js
Normal file
84
src/utils/linkHandler.js
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
// src/utils/linkHandler.js
|
||||
// Utilities for handling meta.link objects and link actions.
|
||||
|
||||
export function openExternal(url) {
|
||||
try {
|
||||
// open in new tab with security flags
|
||||
window.open(url, '_blank', 'noopener,noreferrer');
|
||||
} catch (e) {
|
||||
// fallback: navigate current window
|
||||
window.location.href = url;
|
||||
}
|
||||
}
|
||||
|
||||
export function copyToClipboard(text) {
|
||||
if (!navigator.clipboard) {
|
||||
const el = document.createElement('textarea');
|
||||
el.value = text;
|
||||
el.style.position = 'fixed';
|
||||
el.style.left = '-9999px';
|
||||
document.body.appendChild(el);
|
||||
el.select();
|
||||
try { document.execCommand('copy'); } catch (err) {}
|
||||
document.body.removeChild(el);
|
||||
return Promise.resolve();
|
||||
}
|
||||
return navigator.clipboard.writeText(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to play the url in-app via the host bridge.
|
||||
* - If window.Stremio.play exists, call it.
|
||||
* - Otherwise dispatch a custom event 'stremio-play-link' (repo may listen for it).
|
||||
* - Fallback: open externally after a short delay.
|
||||
*/
|
||||
export async function playInApp(url, meta = {}) {
|
||||
try {
|
||||
if (window.Stremio && typeof window.Stremio.play === 'function') {
|
||||
return window.Stremio.play(url, meta);
|
||||
}
|
||||
// dispatch event for hosting wrapper to pick up
|
||||
const ev = new CustomEvent('stremio-play-link', { detail: { url, meta }});
|
||||
window.dispatchEvent(ev);
|
||||
// fallback to open externally after 350ms if nothing else happens
|
||||
setTimeout(() => openExternal(url), 350);
|
||||
} catch (err) {
|
||||
openExternal(url);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide how to handle a meta.link object.
|
||||
* link = { name, type, url, img, info }
|
||||
*/
|
||||
export async function handleMetaLink(link, meta = {}) {
|
||||
if (!link || !link.url) return;
|
||||
|
||||
const url = String(link.url);
|
||||
const type = String(link.type || '').toLowerCase();
|
||||
|
||||
// treat magnet and torrent files as playable
|
||||
if (url.startsWith('magnet:') || url.endsWith('.torrent')) {
|
||||
return playInApp(url, meta);
|
||||
}
|
||||
|
||||
// If type clearly indicates playable stream
|
||||
const playableTypes = ['stream', 'video', 'play', 'http', 'https', 'magnet', 'torrent', 'm3u8', 'mp4'];
|
||||
if (playableTypes.includes(type)) {
|
||||
return playInApp(url, meta);
|
||||
}
|
||||
|
||||
// types that are clearly external info links
|
||||
const externalTypes = ['imdb', 'website', 'external', 'trailer', 'info', 'purchase'];
|
||||
if (externalTypes.includes(type)) {
|
||||
return openExternal(url);
|
||||
}
|
||||
|
||||
// stremio protocol deep link -> in-app first
|
||||
if (url.startsWith('stremio://')) {
|
||||
return playInApp(url, meta);
|
||||
}
|
||||
|
||||
// default fallback: open externally
|
||||
return openExternal(url);
|
||||
}
|
||||
Loading…
Reference in a new issue