diff --git a/.github/workflows/auto_assign.yml b/.github/workflows/auto_assign.yml new file mode 100644 index 000000000..87e9c8f37 --- /dev/null +++ b/.github/workflows/auto_assign.yml @@ -0,0 +1,65 @@ +name: PR and Issue Workflow +on: + pull_request: + types: [opened, reopened] + issues: + types: [opened] +jobs: + auto-assign-and-label: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + # Auto assign PR to author + - name: Auto Assign PR to Author + if: github.event_name == 'pull_request' + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const pr = context.payload.pull_request; + if (pr) { + await github.rest.issues.addAssignees({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + assignees: [pr.user.login] + }); + console.log(`Assigned PR #${pr.number} to author @${pr.user.login}`); + } + + # Dynamic labeling based on PR/Issue title + - name: Label PRs and Issues + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const prTitle = context.payload.pull_request ? context.payload.pull_request.title : context.payload.issue.title; + const issueNumber = context.payload.pull_request ? context.payload.pull_request.number : context.payload.issue.number; + const isIssue = context.payload.issue !== undefined; + const labelMappings = [ + { pattern: /^feat(ure)?/i, label: 'feature' }, + { pattern: /^fix/i, label: 'bug' }, + { pattern: /^refactor/i, label: 'refactor' }, + { pattern: /^chore/i, label: 'chore' }, + { pattern: /^docs?/i, label: 'documentation' }, + { pattern: /^perf(ormance)?/i, label: 'performance' }, + { pattern: /^test/i, label: 'testing' } + ]; + let labelsToAdd = []; + for (const mapping of labelMappings) { + if (mapping.pattern.test(prTitle)) { + labelsToAdd.push(mapping.label); + } + } + if (labelsToAdd.length > 0) { + github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + labels: labelsToAdd + }); + } \ No newline at end of file diff --git a/src/App/App.js b/src/App/App.js index 38be271ac..3e816be9f 100644 --- a/src/App/App.js +++ b/src/App/App.js @@ -102,12 +102,18 @@ const App = () => { // Handle shell events React.useEffect(() => { const onOpenMedia = (data) => { - if (data.startsWith('stremio:///')) return; - if (data.startsWith('stremio://')) { - const transportUrl = data.replace('stremio://', 'https://'); - if (URL.canParse(transportUrl)) { - window.location.href = `#/addons?addon=${encodeURIComponent(transportUrl)}`; + try { + const { protocol, hostname, pathname, searchParams } = new URL(data); + if (protocol === CONSTANTS.PROTOCOL) { + if (hostname.length) { + const transportUrl = `https://${hostname}${pathname}`; + window.location.href = `#/addons?addon=${encodeURIComponent(transportUrl)}`; + } else { + window.location.href = `#${pathname}?${searchParams.toString()}`; + } } + } catch (e) { + console.error('Failed to open media:', e); } }; diff --git a/src/common/CONSTANTS.js b/src/common/CONSTANTS.js index 8e4e3efdc..92d009895 100644 --- a/src/common/CONSTANTS.js +++ b/src/common/CONSTANTS.js @@ -106,6 +106,8 @@ const EXTERNAL_PLAYERS = [ const WHITELISTED_HOSTS = ['stremio.com', 'strem.io', 'stremio.zendesk.com', 'google.com', 'youtube.com', 'twitch.tv', 'twitter.com', 'x.com', 'netflix.com', 'adex.network', 'amazon.com', 'forms.gle']; +const PROTOCOL = 'stremio:'; + module.exports = { CHROMECAST_RECEIVER_APP_ID, DEFAULT_STREAMING_SERVER_URL, @@ -127,4 +129,5 @@ module.exports = { SUPPORTED_LOCAL_SUBTITLES, EXTERNAL_PLAYERS, WHITELISTED_HOSTS, + PROTOCOL, }; diff --git a/src/common/Platform/Platform.tsx b/src/common/Platform/Platform.tsx index 2212303e4..0da1881ef 100644 --- a/src/common/Platform/Platform.tsx +++ b/src/common/Platform/Platform.tsx @@ -1,6 +1,5 @@ import React, { createContext, useContext } from 'react'; import { WHITELISTED_HOSTS } from 'stremio/common/CONSTANTS'; -import useShell from 'stremio/common/useShell'; import { name, isMobile } from './device'; interface PlatformContext { @@ -16,19 +15,13 @@ type Props = { }; const PlatformProvider = ({ children }: Props) => { - const shell = useShell(); - const openExternal = (url: string) => { try { const { hostname } = new URL(url); const isWhitelisted = WHITELISTED_HOSTS.some((host: string) => hostname.endsWith(host)); const finalUrl = !isWhitelisted ? `https://www.stremio.com/warning#${encodeURIComponent(url)}` : url; - if (shell.active) { - shell.send('open-external', finalUrl); - } else { - window.open(finalUrl, '_blank'); - } + window.open(finalUrl, '_blank'); } catch (e) { console.error('Failed to parse external url:', e); } diff --git a/src/common/routesRegexp.js b/src/common/routesRegexp.js index 3903da44b..b9989b4b5 100644 --- a/src/common/routesRegexp.js +++ b/src/common/routesRegexp.js @@ -6,7 +6,7 @@ const routesRegexp = { urlParamsNames: [] }, board: { - regexp: /^\/?$/, + regexp: /^\/?(?:board)?$/, urlParamsNames: [] }, discover: { diff --git a/src/common/useFullscreen.ts b/src/common/useFullscreen.ts index 5f0975fb8..9bd5d0fc5 100644 --- a/src/common/useFullscreen.ts +++ b/src/common/useFullscreen.ts @@ -46,6 +46,10 @@ const useFullscreen = () => { exitFullscreen(); } + if (event.code === 'KeyF') { + toggleFullscreen(); + } + if (event.code === 'F11' && shell.active) { toggleFullscreen(); } @@ -60,7 +64,7 @@ const useFullscreen = () => { document.removeEventListener('keydown', onKeyDown); document.removeEventListener('fullscreenchange', onFullscreenChange); }; - }, [settings.escExitFullscreen]); + }, [settings.escExitFullscreen, toggleFullscreen]); return [fullscreen, requestFullscreen, exitFullscreen, toggleFullscreen]; }; diff --git a/src/components/MultiselectMenu/Dropdown/Dropdown.tsx b/src/components/MultiselectMenu/Dropdown/Dropdown.tsx index 3da75619b..411ef6ab5 100644 --- a/src/components/MultiselectMenu/Dropdown/Dropdown.tsx +++ b/src/components/MultiselectMenu/Dropdown/Dropdown.tsx @@ -10,23 +10,25 @@ import styles from './Dropdown.less'; type Props = { options: MultiselectMenuOption[]; - selectedOption?: MultiselectMenuOption | null; + value?: string | number; menuOpen: boolean | (() => void); level: number; setLevel: (level: number) => void; - onSelect: (value: number) => void; + onSelect: (value: string | number) => void; }; -const Dropdown = ({ level, setLevel, options, onSelect, selectedOption, menuOpen }: Props) => { +const Dropdown = ({ level, setLevel, options, onSelect, value, menuOpen }: Props) => { const { t } = useTranslation(); const optionsRef = useRef(new Map()); const containerRef = useRef(null); - const handleSetOptionRef = useCallback((value: number) => (node: HTMLButtonElement | null) => { + const selectedOption = options.find((opt) => opt.value === value); + + const handleSetOptionRef = useCallback((optionValue: string | number) => (node: HTMLButtonElement | null) => { if (node) { - optionsRef.current.set(value, node); + optionsRef.current.set(optionValue, node); } else { - optionsRef.current.delete(value); + optionsRef.current.delete(optionValue); } }, []); @@ -63,11 +65,11 @@ const Dropdown = ({ level, setLevel, options, onSelect, selectedOption, menuOpen .filter((option: MultiselectMenuOption) => !option.hidden) .map((option: MultiselectMenuOption) => (