mirror of
https://github.com/ThatGuySam/doesitarm.git
synced 2026-05-18 06:44:46 -07:00
Merge branch 'feat/tv' into develop
This commit is contained in:
commit
719947c76e
30 changed files with 2239 additions and 112 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -86,5 +86,6 @@ dist
|
||||||
/README-temp.md
|
/README-temp.md
|
||||||
/static/game-list.json
|
/static/game-list.json
|
||||||
/static/homebrew-list.json
|
/static/homebrew-list.json
|
||||||
.DS_Store
|
/static/video-list.json
|
||||||
/commits-data.json
|
/commits-data.json
|
||||||
|
.DS_Store
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ Any comments, suggestions? [Let us know!](https://github.com/ThatGuySam/doesitar
|
||||||
* [Attributed String Creator](https://apps.apple.com/us/app/attributed-string-creator-pro/id730928349) - ✅ Yes, full native support as of v1.9.6 - [Release Notes](https://www.bridgetech.io)
|
* [Attributed String Creator](https://apps.apple.com/us/app/attributed-string-creator-pro/id730928349) - ✅ Yes, full native support as of v1.9.6 - [Release Notes](https://www.bridgetech.io)
|
||||||
* [BBEdit](https://www.barebones.com/products/bbedit/download.html) - ✅ Yes, full native support as of v13.5 - [Release Notes](https://www.barebones.com/support/bbedit/notes-13.5.html)
|
* [BBEdit](https://www.barebones.com/products/bbedit/download.html) - ✅ Yes, full native support as of v13.5 - [Release Notes](https://www.barebones.com/support/bbedit/notes-13.5.html)
|
||||||
* [Beyond Compare](https://www.scootersoftware.com/) - ✳️ Yes, works via Rosetta 2 - [Facebook Post](https://www.facebook.com/ScooterSoftware/posts/5178865142127412)
|
* [Beyond Compare](https://www.scootersoftware.com/) - ✳️ Yes, works via Rosetta 2 - [Facebook Post](https://www.facebook.com/ScooterSoftware/posts/5178865142127412)
|
||||||
|
* [Charles Web Debugging Proxy](https://www.charlesproxy.com/download/) - 🔶 Unknown, more info needed - [Contribute](https://github.com/ThatGuySam/doesitarm/issues/122)
|
||||||
* [CocoaPods](https://cocoapods.org/) - ✳️ Yes, it works via Rosetta 2 - [Issue](https://github.com/CocoaPods/CocoaPods/issues/9907)
|
* [CocoaPods](https://cocoapods.org/) - ✳️ Yes, it works via Rosetta 2 - [Issue](https://github.com/CocoaPods/CocoaPods/issues/9907)
|
||||||
* [CotEditor](https://coteditor.com) - ✅ Yes, full native support as of 4.0.0 - [App Store](https://itunes.apple.com/app/coteditor/id1024640650)
|
* [CotEditor](https://coteditor.com) - ✅ Yes, full native support as of 4.0.0 - [App Store](https://itunes.apple.com/app/coteditor/id1024640650)
|
||||||
* [Cyberduck](https://cyberduck.io/download/) - ✳️ Yes, works via Rosetta 2 with native build in development - [Source](https://github.com/ThatGuySam/doesitarm/issues/333)
|
* [Cyberduck](https://cyberduck.io/download/) - ✳️ Yes, works via Rosetta 2 with native build in development - [Source](https://github.com/ThatGuySam/doesitarm/issues/333)
|
||||||
|
|
@ -44,6 +45,7 @@ Any comments, suggestions? [Let us know!](https://github.com/ThatGuySam/doesitar
|
||||||
* [Filezilla](https://filezilla-project.org/) - ✳️ Yes, works via Rosetta 2 - [Verification](https://github.com/ThatGuySam/doesitarm/issues/17#issuecomment-729976000)
|
* [Filezilla](https://filezilla-project.org/) - ✳️ Yes, works via Rosetta 2 - [Verification](https://github.com/ThatGuySam/doesitarm/issues/17#issuecomment-729976000)
|
||||||
* [Flutter](https://flutter.dev/docs/get-started/install/macos) - ✳️ Yes, works via Rosetta 2 with native support in development - [Github Issue](https://github.com/flutter/flutter/issues/60118#issuecomment-695341296)
|
* [Flutter](https://flutter.dev/docs/get-started/install/macos) - ✳️ Yes, works via Rosetta 2 with native support in development - [Github Issue](https://github.com/flutter/flutter/issues/60118#issuecomment-695341296)
|
||||||
* [Fork](https://git-fork.com/) - ✅ Yes, full native support as of v2.1.0 - [Release notes](https://git-fork.com/releasenotes)
|
* [Fork](https://git-fork.com/) - ✅ Yes, full native support as of v2.1.0 - [Release notes](https://git-fork.com/releasenotes)
|
||||||
|
* [GCC ARM Embedded](https://developer.arm.com/tools-and-software/open-source-software/developer-tools/gnu-toolchain/gnu-rm) - ✳️ Yes, runs via Rosetta 2 - [Verification](https://github.com/ThatGuySam/doesitarm/issues/105#issuecomment-750419946)
|
||||||
* [Git Version Control](https://git-scm.com/download/mac) - ✅ Yes, Full Native Apple Silicon Support - [Source](https://github.com/ThatGuySam/doesitarm/issues/54#issuecomment-730568063)
|
* [Git Version Control](https://git-scm.com/download/mac) - ✅ Yes, Full Native Apple Silicon Support - [Source](https://github.com/ThatGuySam/doesitarm/issues/54#issuecomment-730568063)
|
||||||
* [GitHub Desktop](https://desktop.github.com/) - ✳️ Yes, works via Rosetta 2 as of v2.6.0 with native support in development - [GitHub issue](https://github.com/ThatGuySam/doesitarm/issues/293)
|
* [GitHub Desktop](https://desktop.github.com/) - ✳️ Yes, works via Rosetta 2 as of v2.6.0 with native support in development - [GitHub issue](https://github.com/ThatGuySam/doesitarm/issues/293)
|
||||||
* [Go (golang)](https://golang.org/) - ✳️ Runs via Rosetta 2, with native builds available in beta - [Golang M1 Benchmark](https://docs.google.com/spreadsheets/d/1g4U7LAImfEcXRihJbySZcRr32tn6WSWAtslfXltds58/edit#gid=342445681) [Issue](https://github.com/golang/go/issues/38485) [Beta Download](https://golang.org/dl/#go1.16beta1) [Release Notes](https://tip.golang.org/doc/go1.16)
|
* [Go (golang)](https://golang.org/) - ✳️ Runs via Rosetta 2, with native builds available in beta - [Golang M1 Benchmark](https://docs.google.com/spreadsheets/d/1g4U7LAImfEcXRihJbySZcRr32tn6WSWAtslfXltds58/edit#gid=342445681) [Issue](https://github.com/golang/go/issues/38485) [Beta Download](https://golang.org/dl/#go1.16beta1) [Release Notes](https://tip.golang.org/doc/go1.16)
|
||||||
|
|
@ -56,6 +58,7 @@ Any comments, suggestions? [Let us know!](https://github.com/ThatGuySam/doesitar
|
||||||
* [IntelliJ IDEA](https://www.jetbrains.com/idea/download/#section=mac) - ✳️ Runs via Rosetta 2, native support available as preview - [Official Jetbrains Issue](https://youtrack.jetbrains.com/issue/JBR-2526) [Download Preview](https://youtrack.jetbrains.com/issue/JBR-2526#focus=Comments-27-4589077.0-0)
|
* [IntelliJ IDEA](https://www.jetbrains.com/idea/download/#section=mac) - ✳️ Runs via Rosetta 2, native support available as preview - [Official Jetbrains Issue](https://youtrack.jetbrains.com/issue/JBR-2526) [Download Preview](https://youtrack.jetbrains.com/issue/JBR-2526#focus=Comments-27-4589077.0-0)
|
||||||
* [iTerm](https://iterm2.com/downloads.html) - ✅ Yes, fully supported as of v3.4.0 - [PR](https://github.com/gnachman/iTerm2/pull/421)
|
* [iTerm](https://iterm2.com/downloads.html) - ✅ Yes, fully supported as of v3.4.0 - [PR](https://github.com/gnachman/iTerm2/pull/421)
|
||||||
* [Julia Language](https://julialang.org/downloads/) - ✳️ Yes, it works via Rosetta 2 - [Github Issue](https://github.com/JuliaLang/julia/issues/36617)
|
* [Julia Language](https://julialang.org/downloads/) - ✳️ Yes, it works via Rosetta 2 - [Github Issue](https://github.com/JuliaLang/julia/issues/36617)
|
||||||
|
* [KiCad EDA](https://kicad.org/download/macos/) - ✳️ Yes, works via Rosetta 2 - [Verification](https://github.com/ThatGuySam/doesitarm/issues/199#issuecomment-736253625)
|
||||||
* [LLVM Clang](https://releases.llvm.org/download.html) - ✳️ Yes, it works via Rosetta 2 - [Apple Forums](https://developer.apple.com/forums/thread/649992)
|
* [LLVM Clang](https://releases.llvm.org/download.html) - ✳️ Yes, it works via Rosetta 2 - [Apple Forums](https://developer.apple.com/forums/thread/649992)
|
||||||
* [MacDown](https://macdown.uranusjr.com/) - ✳️ Yes, works via Rosetta 2 - [Verification](https://github.com/ThatGuySam/doesitarm/issues/382)
|
* [MacDown](https://macdown.uranusjr.com/) - ✳️ Yes, works via Rosetta 2 - [Verification](https://github.com/ThatGuySam/doesitarm/issues/382)
|
||||||
* [MacPorts](https://www.macports.org/install.php) - ✳️ Yes, some ports are native while others work via Rosetta 2. - [Discussion](https://github.com/ThatGuySam/doesitarm/issues/302).
|
* [MacPorts](https://www.macports.org/install.php) - ✳️ Yes, some ports are native while others work via Rosetta 2. - [Discussion](https://github.com/ThatGuySam/doesitarm/issues/302).
|
||||||
|
|
@ -181,6 +184,7 @@ Any comments, suggestions? [Let us know!](https://github.com/ThatGuySam/doesitar
|
||||||
* [Handbrake](https://handbrake.fr/) - ✅ Yes, natively supported as of v1.4.0 - [Github Issue](https://github.com/HandBrake/HandBrake/issues/2951)
|
* [Handbrake](https://handbrake.fr/) - ✅ Yes, natively supported as of v1.4.0 - [Github Issue](https://github.com/HandBrake/HandBrake/issues/2951)
|
||||||
* [MKVToolNix](https://mkvtoolnix.download/downloads.html#macosx) - ✳️ Yes, works via Rosetta 2 - [GitHub issue](https://github.com/ThatGuySam/doesitarm/issues/344)
|
* [MKVToolNix](https://mkvtoolnix.download/downloads.html#macosx) - ✳️ Yes, works via Rosetta 2 - [GitHub issue](https://github.com/ThatGuySam/doesitarm/issues/344)
|
||||||
* [OBS](https://obsproject.com/) - ✳️ Yes, works via Rosetta 2 - [MacRumors Discussion](https://forums.macrumors.com/threads/so-hows-m1-for-streamers-obs-streamlabs-obs-etc.2269239/) [Mention in Issue](https://github.com/obsproject/obs-studio/pull/3444#issuecomment-690216403)
|
* [OBS](https://obsproject.com/) - ✳️ Yes, works via Rosetta 2 - [MacRumors Discussion](https://forums.macrumors.com/threads/so-hows-m1-for-streamers-obs-streamlabs-obs-etc.2269239/) [Mention in Issue](https://github.com/obsproject/obs-studio/pull/3444#issuecomment-690216403)
|
||||||
|
* [OpenISS](https://github.com/OpenISS/OpenISS) - 🔶 Unknown, more info needed - [GitHub Issue](https://github.com/OpenISS/OpenISS/issues/72) [Contribute](https://github.com/ThatGuySam/doesitarm/issues/475)
|
||||||
* [Premiere Pro](https://www.adobe.com/products/premiere.html) - ✳️ Yes, works via Rosetta 2 - [Official Adobe Status Page](https://helpx.adobe.com/download-install/kb/apple-silicon-m1-chip.html)
|
* [Premiere Pro](https://www.adobe.com/products/premiere.html) - ✳️ Yes, works via Rosetta 2 - [Official Adobe Status Page](https://helpx.adobe.com/download-install/kb/apple-silicon-m1-chip.html)
|
||||||
* [Premiere Rush](https://www.adobe.com/products/premiere-rush.html) - ✳️ Yes, works via Rosetta 2 - [Official Adobe Status Page](https://helpx.adobe.com/download-install/kb/apple-silicon-m1-chip.html)
|
* [Premiere Rush](https://www.adobe.com/products/premiere-rush.html) - ✳️ Yes, works via Rosetta 2 - [Official Adobe Status Page](https://helpx.adobe.com/download-install/kb/apple-silicon-m1-chip.html)
|
||||||
* [Tumult Hype](https://tumult.com/hype/) - ✅ Yes, Full Native Apple Silicon Support as of v4.1 - [Blog Post](https://blog.tumult.com/2020/11/23/introducing-tumult-hype-v4-1-with-apple-silicon-and-big-sur-compatibility/)
|
* [Tumult Hype](https://tumult.com/hype/) - ✅ Yes, Full Native Apple Silicon Support as of v4.1 - [Blog Post](https://blog.tumult.com/2020/11/23/introducing-tumult-hype-v4-1-with-apple-silicon-and-big-sur-compatibility/)
|
||||||
|
|
@ -218,6 +222,7 @@ Any comments, suggestions? [Let us know!](https://github.com/ThatGuySam/doesitar
|
||||||
* [BetterTouchTool](https://folivora.ai/) - ✅ Yes, fully supported as of v3.502 - [Issue Tracker](https://community.folivora.ai/)
|
* [BetterTouchTool](https://folivora.ai/) - ✅ Yes, fully supported as of v3.502 - [Issue Tracker](https://community.folivora.ai/)
|
||||||
* [Blackmagic Disk Speed Test](https://apps.apple.com/app/id425264550) - ✅ Yes, Full Native Apple Silicon Support - [Verification](https://github.com/ThatGuySam/doesitarm/issues/359#issuecomment-736255914)
|
* [Blackmagic Disk Speed Test](https://apps.apple.com/app/id425264550) - ✅ Yes, Full Native Apple Silicon Support - [Verification](https://github.com/ThatGuySam/doesitarm/issues/359#issuecomment-736255914)
|
||||||
* [Box Drive](https://www.box.com/resources/downloads) - ⏹ Not yet, but it's currently in development. - [Official Post](https://support.box.com/hc/en-us/community/posts/360051323454-Box-Drive-s-system-extension-failed-to-load?page=1#community_comment_1500000009302)
|
* [Box Drive](https://www.box.com/resources/downloads) - ⏹ Not yet, but it's currently in development. - [Official Post](https://support.box.com/hc/en-us/community/posts/360051323454-Box-Drive-s-system-extension-failed-to-load?page=1#community_comment_1500000009302)
|
||||||
|
* [Calibre](https://calibre-ebook.com/download_osx) - ✳️ Yes, works via Rosetta 2 - [Verification](https://github.com/ThatGuySam/doesitarm/issues/26#issuecomment-736778254)
|
||||||
* [Chrome](https://www.google.com/chrome/) - ✅ Yes, fully supported as of v87 - [Article](https://9to5google.com/2020/11/17/chrome-mac-apple-silicon/)
|
* [Chrome](https://www.google.com/chrome/) - ✅ Yes, fully supported as of v87 - [Article](https://9to5google.com/2020/11/17/chrome-mac-apple-silicon/)
|
||||||
* [coconutBattery](https://www.coconut-flavour.com/coconutbattery/) - ✅ Yes, full native support as of v3.9.2
|
* [coconutBattery](https://www.coconut-flavour.com/coconutbattery/) - ✅ Yes, full native support as of v3.9.2
|
||||||
* [Coloban](https://www.coloban.com) - ⏹ Not yet, but it's currently in development. - [Coloban Forum Issue](https://forum.coloban.com/index.php?u=/topic/21/new-arm-based-apple-computers)
|
* [Coloban](https://www.coloban.com) - ⏹ Not yet, but it's currently in development. - [Coloban Forum Issue](https://forum.coloban.com/index.php?u=/topic/21/new-arm-based-apple-computers)
|
||||||
|
|
@ -264,6 +269,7 @@ Any comments, suggestions? [Let us know!](https://github.com/ThatGuySam/doesitar
|
||||||
* [Notability](https://www.gingerlabs.com/) - ✳️ Yes, works via Rosetta 2 - [Verification](https://github.com/ThatGuySam/doesitarm/issues/417#issue-760864996)
|
* [Notability](https://www.gingerlabs.com/) - ✳️ Yes, works via Rosetta 2 - [Verification](https://github.com/ThatGuySam/doesitarm/issues/417#issue-760864996)
|
||||||
* [Notion Desktop](https://www.notion.so) - ✅ Yes, Full Native Apple Silicon Support - [Official Tweet](https://twitter.com/NotionHQ/status/1333867094463582208?ref_src=twsrc%5Egoogle%7Ctwcamp%5Eserp%7Ctwgr%5Etweet) [Verification](https://github.com/ThatGuySam/doesitarm/issues/378#issue-755529762)
|
* [Notion Desktop](https://www.notion.so) - ✅ Yes, Full Native Apple Silicon Support - [Official Tweet](https://twitter.com/NotionHQ/status/1333867094463582208?ref_src=twsrc%5Egoogle%7Ctwcamp%5Eserp%7Ctwgr%5Etweet) [Verification](https://github.com/ThatGuySam/doesitarm/issues/378#issue-755529762)
|
||||||
* [Noto](http://noto.ink/) - ✅ Yes, Full Native Apple Silicon Support - [App Store Story](https://apps.apple.com/us/story/id1540024103)
|
* [Noto](http://noto.ink/) - ✅ Yes, Full Native Apple Silicon Support - [App Store Story](https://apps.apple.com/us/story/id1540024103)
|
||||||
|
* [Obsidian](https://obsidian.md/download) - ✅ Yes, full native support as of v0.10.1
|
||||||
* [OmniFocus](https://www.omnigroup.com/omnifocus) - ✅ Yes, Full Native Apple Silicon Support as of v3.10 - [Release Notes](https://www.omnigroup.com/releasenotes/omnifocus)
|
* [OmniFocus](https://www.omnigroup.com/omnifocus) - ✅ Yes, Full Native Apple Silicon Support as of v3.10 - [Release Notes](https://www.omnigroup.com/releasenotes/omnifocus)
|
||||||
* [OmniGraffle](https://www.omnigroup.com/omnigraffle) - ✅ Yes, Full Native Apple Silicon Support as of v7.18 - [Release Notes](https://www.omnigroup.com/releasenotes/omnigraffle)
|
* [OmniGraffle](https://www.omnigroup.com/omnigraffle) - ✅ Yes, Full Native Apple Silicon Support as of v7.18 - [Release Notes](https://www.omnigroup.com/releasenotes/omnigraffle)
|
||||||
* [OmniOutliner](https://www.omnigroup.com/omnioutliner) - ✅ Yes, Full Native Apple Silicon Support as of v5.8 - [Release Notes](https://www.omnigroup.com/releasenotes/omnioutliner)
|
* [OmniOutliner](https://www.omnigroup.com/omnioutliner) - ✅ Yes, Full Native Apple Silicon Support as of v5.8 - [Release Notes](https://www.omnigroup.com/releasenotes/omnioutliner)
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,26 @@
|
||||||
* @import "utilities/skew-transforms";
|
* @import "utilities/skew-transforms";
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
.ease {
|
||||||
|
transition-property: all;
|
||||||
|
transition-duration: 400ms;
|
||||||
|
/* easeInOutQuart */
|
||||||
|
/* https://easings.net/en#easeInOutQuart */
|
||||||
|
transition-timing-function: cubic-bezier(0.77, 0, 0.175, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lazyload,
|
||||||
|
.lazyloading {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.lazyloaded {
|
||||||
|
@apply ease;
|
||||||
|
transition-property: opacity;
|
||||||
|
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.neumorphic-shadow,
|
.neumorphic-shadow,
|
||||||
.hover\:neumorphic-shadow:hover {
|
.hover\:neumorphic-shadow:hover {
|
||||||
/* box-shadow: -0.25rem -0.25rem 0.5rem rgba(255, 255, 255, 0.07); */
|
/* box-shadow: -0.25rem -0.25rem 0.5rem rgba(255, 255, 255, 0.07); */
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@
|
||||||
:href="href"
|
:href="href"
|
||||||
:target="target"
|
:target="target"
|
||||||
:rel="rel"
|
:rel="rel"
|
||||||
|
:class="classlist"
|
||||||
role="button"
|
role="button"
|
||||||
class="relative inline-flex items-center px-4 py-2 border border-transparent leading-5 font-medium rounded-md text-white bg-darker neumorphic-shadow hover:bg-indigo-400 focus:outline-none focus:shadow-outline-indigo focus:border-indigo-600 active:bg-indigo-600 transition duration-150 ease-in-out"
|
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -23,6 +23,10 @@ export default {
|
||||||
target: {
|
target: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null
|
default: null
|
||||||
|
},
|
||||||
|
classGroups: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -30,6 +34,30 @@ export default {
|
||||||
if (this.href.charAt(0) === '/') return null
|
if (this.href.charAt(0) === '/') return null
|
||||||
|
|
||||||
return 'noopener'
|
return 'noopener'
|
||||||
|
},
|
||||||
|
classlist () {
|
||||||
|
const defaultClassGroups = {
|
||||||
|
general: 'relative inline-flex items-center rounded-md px-4 py-2',
|
||||||
|
font: 'leading-5 font-medium',
|
||||||
|
text: 'text-white',
|
||||||
|
border: 'border border-transparent focus:outline-none focus:border-indigo-600',
|
||||||
|
shadow: 'neumorphic-shadow focus:shadow-outline-indigo',
|
||||||
|
bg: 'bg-darker hover:bg-indigo-400 active:bg-indigo-600',
|
||||||
|
transition: 'transition duration-150 ease-in-out'
|
||||||
|
}
|
||||||
|
|
||||||
|
const mergedClassGroups = {
|
||||||
|
...defaultClassGroups,
|
||||||
|
...this.classGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (this.isFocused) {
|
||||||
|
// delete mergedClassGroups.blur
|
||||||
|
// } else {
|
||||||
|
// delete mergedClassGroups.focus
|
||||||
|
// }
|
||||||
|
|
||||||
|
return Object.values(mergedClassGroups)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -146,6 +146,10 @@ export default {
|
||||||
label: 'Categories',
|
label: 'Categories',
|
||||||
url: '/categories',
|
url: '/categories',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Benchmarks',
|
||||||
|
url: '/benchmarks',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Homebrew',
|
label: 'Homebrew',
|
||||||
url: '/kind/homebrew',
|
url: '/kind/homebrew',
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@
|
||||||
<!-- app.endpoint: {{ app.endpoint }} -->
|
<!-- app.endpoint: {{ app.endpoint }} -->
|
||||||
<a
|
<a
|
||||||
:href="getAppEndpoint(app)"
|
:href="getAppEndpoint(app)"
|
||||||
class="flex flex-col justify-center inset-x-0 hover:bg-darkest border-2 border-white border-opacity-0 hover:border-opacity-50 focus:outline-none focus:bg-gray-50 duration-300 ease-in-out rounded-lg space-y-2 -mx-5 pl-5 md:pl-20 pr-6 md:pr-64 py-6 "
|
class="flex flex-col justify-center inset-x-0 hover:bg-darkest border-2 border-white border-opacity-0 hover:border-opacity-50 focus:outline-none focus:bg-gray-50 duration-300 ease-in-out rounded-lg space-y-2 -mx-5 pl-5 md:pl-20 pr-6 md:pr-64 py-5"
|
||||||
style="transition-property: border;"
|
style="transition-property: border;"
|
||||||
>
|
>
|
||||||
<template v-if="seenItems[app.slug] === false && hasStartedAnyQuery === false">
|
<template v-if="seenItems[app.slug] === false && hasStartedAnyQuery === false">
|
||||||
|
|
@ -118,48 +118,71 @@
|
||||||
</small>
|
</small>
|
||||||
</client-only>
|
</client-only>
|
||||||
|
|
||||||
<client-only>
|
|
||||||
<svg
|
|
||||||
class="absolute right-0 h-5 w-5 text-gray-400"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
>
|
|
||||||
<use href="#chevron-right" />
|
|
||||||
</svg>
|
|
||||||
</client-only>
|
|
||||||
|
|
||||||
<!-- app.endpoint: {{ app.endpoint }} -->
|
<!-- app.endpoint: {{ app.endpoint }} -->
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<!-- <client-only v-if="seenItems[app.slug] || hasStartedAnyQuery">
|
|
||||||
<div
|
|
||||||
class="search-item-options relative md:absolute md:inset-0 w-full pointer-events-none"
|
|
||||||
>
|
|
||||||
|
|
||||||
<div class="search-item-options-container h-full flex justify-center md:justify-end items-center pb-4 md:py-4 md:px-12">
|
<div
|
||||||
|
class="search-item-options relative md:absolute md:inset-0 w-full pointer-events-none"
|
||||||
|
>
|
||||||
|
|
||||||
<div
|
<div class="search-item-options-container h-full flex justify-center md:justify-end items-center pb-4 md:py-4 md:px-4">
|
||||||
v-if="!app.endpoint.includes('/game/')"
|
|
||||||
class="subscribe space-y-6 sm:space-x-6"
|
|
||||||
>
|
|
||||||
<EmailSubscribe
|
|
||||||
:app-name="app.name"
|
|
||||||
:input-class-groups="{
|
|
||||||
shadow: 'hover:neumorphic-shadow',
|
|
||||||
bg: '',
|
|
||||||
focus: 'bg-transparent neumorphic-shadow pl-8',
|
|
||||||
blur: 'placeholder-white text-center border border-transparent bg-transparent opacity-50 hover:opacity-100 px-3',
|
|
||||||
}"
|
|
||||||
class="pointer-events-auto"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
<LinkButton
|
||||||
|
v-for="link in getSearchLinks(app)"
|
||||||
|
:key="`${app.slug}-${link.label.toLowerCase()}`"
|
||||||
|
:href="link.href"
|
||||||
|
:class="[
|
||||||
|
'px-3 py-2 rounded-md text-sm pointer-events-auto focus:outline-none focus:text-white focus:bg-gray-700 transition duration-150 ease-in-out',
|
||||||
|
'text-gray-300 hover:bg-darker hover:neumorphic-shadow'
|
||||||
|
]"
|
||||||
|
:class-groups="{
|
||||||
|
// general: 'relative inline-flex items-center rounded-md px-4 py-2',
|
||||||
|
// font: 'leading-5 font-medium',
|
||||||
|
// text: 'text-white',
|
||||||
|
// border: 'border border-transparent focus:outline-none focus:border-indigo-600',
|
||||||
|
shadow: 'hover:neumorphic-shadow',
|
||||||
|
bg: 'hover:bg-darker',
|
||||||
|
// transition: 'transition duration-150 ease-in-out'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ link.label }}
|
||||||
|
</LinkButton>
|
||||||
|
|
||||||
|
<LinkButton
|
||||||
|
:href="getAppEndpoint(app)"
|
||||||
|
:class="[
|
||||||
|
'px-3 py-2 rounded-md text-sm pointer-events-auto focus:outline-none focus:text-white focus:bg-gray-700 transition duration-150 ease-in-out',
|
||||||
|
'text-gray-300 hover:bg-darker hover:neumorphic-shadow'
|
||||||
|
]"
|
||||||
|
:class-groups="{
|
||||||
|
// general: 'relative inline-flex items-center rounded-md px-4 py-2',
|
||||||
|
// font: 'leading-5 font-medium',
|
||||||
|
// text: 'text-white',
|
||||||
|
// border: 'border border-transparent focus:outline-none focus:border-indigo-600',
|
||||||
|
shadow: 'hover:neumorphic-shadow',
|
||||||
|
bg: 'hover:bg-darker',
|
||||||
|
// transition: 'transition duration-150 ease-in-out'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<span>Details</span>
|
||||||
|
<client-only>
|
||||||
|
<svg
|
||||||
|
class="h-5 w-5 -mr-2"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<use href="#chevron-right" />
|
||||||
|
</svg>
|
||||||
|
</client-only>
|
||||||
|
</LinkButton>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</client-only> -->
|
|
||||||
|
</div>
|
||||||
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
|
@ -174,7 +197,7 @@ import { getAppCategory } from '~/helpers/categories.js'
|
||||||
import { getAppEndpoint } from '~/helpers/app-derived.js'
|
import { getAppEndpoint } from '~/helpers/app-derived.js'
|
||||||
// import appList from '~/static/app-list.json'
|
// import appList from '~/static/app-list.json'
|
||||||
|
|
||||||
// import EmailSubscribe from '~/components/email-subscribe.vue'
|
import LinkButton from '~/components/link-button.vue'
|
||||||
// import RelativeTime from '~/components/relative-time.vue'
|
// import RelativeTime from '~/components/relative-time.vue'
|
||||||
import ListSummary from '~/components/list-summary.vue'
|
import ListSummary from '~/components/list-summary.vue'
|
||||||
|
|
||||||
|
|
@ -183,6 +206,7 @@ export default {
|
||||||
components: {
|
components: {
|
||||||
// EmailSubscribe: () => process.client ? import('~/components/email-subscribe.vue') : null,
|
// EmailSubscribe: () => process.client ? import('~/components/email-subscribe.vue') : null,
|
||||||
ListSummary,
|
ListSummary,
|
||||||
|
LinkButton,
|
||||||
RelativeTime: () => process.client ? import('~/components/relative-time.vue') : null
|
RelativeTime: () => process.client ? import('~/components/relative-time.vue') : null
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
|
@ -325,6 +349,11 @@ export default {
|
||||||
methods: {
|
methods: {
|
||||||
getAppCategory,
|
getAppCategory,
|
||||||
getAppEndpoint,
|
getAppEndpoint,
|
||||||
|
getSearchLinks (app) {
|
||||||
|
if (typeof app.searchLinks === 'undefined') return []
|
||||||
|
|
||||||
|
return app.searchLinks
|
||||||
|
},
|
||||||
// Search priorities
|
// Search priorities
|
||||||
titleStartsWith (query, app) {
|
titleStartsWith (query, app) {
|
||||||
const matches = app.name.toLowerCase().startsWith(query)
|
const matches = app.name.toLowerCase().startsWith(query)
|
||||||
|
|
|
||||||
133
components/video/bg-player.vue
Normal file
133
components/video/bg-player.vue
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:style="{
|
||||||
|
'left': '50%',
|
||||||
|
'right': '50%',
|
||||||
|
'margin-left': '-50vw',
|
||||||
|
'margin-right': '-50vw',
|
||||||
|
'mask-image': 'linear-gradient(to top, rgba(0, 0, 0, 0) 25%, rgba(0, 0, 0, 0.25))',
|
||||||
|
opacity: playing ? 1 : 0
|
||||||
|
}"
|
||||||
|
class="video-canvas w-screen flex justify-center bg-black transition-opacity duration-500 ease-in-out"
|
||||||
|
>
|
||||||
|
<div class="ratio-wrapper w-full">
|
||||||
|
<div class="relative overflow-hidden w-full pb-16/9">
|
||||||
|
<iframe
|
||||||
|
ref="frame"
|
||||||
|
:id="frameId"
|
||||||
|
:src="`https://www.youtube.com/embed/${video.id}?enablejsapi=1&autoplay=1&modestbranding=1&playsinline=1&start=0&end=30&loop=1`"
|
||||||
|
:style="{
|
||||||
|
height: '200%',
|
||||||
|
top: '50%',
|
||||||
|
transform: 'translateY(-50%)'
|
||||||
|
}"
|
||||||
|
class="absolute w-full object-cover"
|
||||||
|
frameborder="0"
|
||||||
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||||
|
allowfullscreen
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
// components: {
|
||||||
|
// VideoCard
|
||||||
|
// },
|
||||||
|
props: {
|
||||||
|
video: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data: function() {
|
||||||
|
return {
|
||||||
|
player: null,
|
||||||
|
playing: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
frameId () {
|
||||||
|
return `youtube-bg-${this._uid}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
// Set frame ID here so that it's the same when Youtube API looks for it
|
||||||
|
// this.frameId = `youtube-bg-${this._uid}`
|
||||||
|
|
||||||
|
const tag = document.createElement('script')
|
||||||
|
tag.id = `youtube-bg-script-${this._uid}`
|
||||||
|
tag.src = 'https://www.youtube.com/iframe_api'
|
||||||
|
|
||||||
|
const firstScriptTag = document.getElementsByTagName('script')[0]
|
||||||
|
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag)
|
||||||
|
|
||||||
|
|
||||||
|
window.onYouTubeIframeAPIReady = this.onYouTubeIframeAPIReady
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
restartVideo () {
|
||||||
|
this.player.seekTo(0)
|
||||||
|
this.player.playVideo()
|
||||||
|
},
|
||||||
|
|
||||||
|
onPlayerEnd () {
|
||||||
|
console.log('Video ended')
|
||||||
|
|
||||||
|
this.restartVideo()
|
||||||
|
},
|
||||||
|
onPlayerPlaying () {
|
||||||
|
console.log('Player playing')
|
||||||
|
|
||||||
|
this.playing = true
|
||||||
|
},
|
||||||
|
onPlayerReady (event) {
|
||||||
|
console.log('Player is ready', this.player)
|
||||||
|
|
||||||
|
// Mute the player
|
||||||
|
this.player.mute()
|
||||||
|
|
||||||
|
this.player.playVideo()
|
||||||
|
},
|
||||||
|
onYouTubeIframeAPIReady () {
|
||||||
|
// console.log('Youtube Embed API Ready')
|
||||||
|
|
||||||
|
const stateHandlers = {
|
||||||
|
// unstarted
|
||||||
|
'-1': () => null,
|
||||||
|
// ended
|
||||||
|
'0': this.onPlayerEnd,
|
||||||
|
// playing
|
||||||
|
'1': this.onPlayerPlaying,
|
||||||
|
// paused
|
||||||
|
'2': () => null,
|
||||||
|
// buffering
|
||||||
|
'3': () => null,
|
||||||
|
// video cued
|
||||||
|
'4': () => null,
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log('YT', YT)
|
||||||
|
|
||||||
|
// console.log('frame', this.$refs['frame'])
|
||||||
|
// console.log('frame id', this.$refs['frame'].id)
|
||||||
|
|
||||||
|
this.player = new YT.Player(this.$refs['frame'].id, {
|
||||||
|
events: {
|
||||||
|
'onReady': this.onPlayerReady,
|
||||||
|
'onStateChange': event => {
|
||||||
|
// console.log('state changed', event)
|
||||||
|
|
||||||
|
const stateHandler = stateHandlers[String(event.data)]
|
||||||
|
// console.log('stateHandler', stateHandler)
|
||||||
|
stateHandler(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
108
components/video/card.vue
Normal file
108
components/video/card.vue
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
<template>
|
||||||
|
|
||||||
|
<div class="video-card">
|
||||||
|
<a
|
||||||
|
:href="video.endpoint"
|
||||||
|
class=""
|
||||||
|
>
|
||||||
|
<div class="video-card-container relative overflow-hidden bg-black">
|
||||||
|
<div class="video-card-image ratio-wrapper">
|
||||||
|
<div class="relative overflow-hidden w-full pb-16/9">
|
||||||
|
<picture>
|
||||||
|
<source
|
||||||
|
:sizes="thumbnailSizes"
|
||||||
|
:data-srcset="thumbnailSrcset"
|
||||||
|
type="image/jpg"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:data-src="video.thumbnails.default.url"
|
||||||
|
:alt="video.name"
|
||||||
|
class="lazyload absolute h-full w-full object-cover"
|
||||||
|
>
|
||||||
|
</picture>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
:style="{
|
||||||
|
'--gradient-from-color': 'rgba(0, 0, 0, 1)',
|
||||||
|
'--gradient-to-color': 'rgba(0, 0, 0, 0.7)'
|
||||||
|
}"
|
||||||
|
class="video-card-overlay absolute inset-0 flex justify-between items-start bg-gradient-to-tr from-black to-transparent p-4"
|
||||||
|
>
|
||||||
|
<div class="play-circle w-8 h-8 bg-white-2 flex justify-center items-center outline-0 rounded-full ease">
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 18 18"
|
||||||
|
style="width:18px;height:18px;margin-left:3px"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M15.562 8.1L3.87.225c-.818-.562-1.87 0-1.87.9v15.75c0 .9 1.052 1.462 1.87.9L15.563 9.9c.584-.45.584-1.35 0-1.8z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="pill"
|
||||||
|
class="video-pill h-5 text-xs bg-white-2 flex justify-center items-center outline-0 rounded-full ease px-2"
|
||||||
|
>
|
||||||
|
{{ pill }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Video Text Content -->
|
||||||
|
<div class="video-card-content absolute inset-0 flex items-end py-4 px-6">
|
||||||
|
<div class="w-full text-sm text-left whitespace-normal">{{ video.name }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import 'lazysizes'
|
||||||
|
|
||||||
|
// import { getVideoEndpoint } from '~/helpers/app-derived.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
video: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
thumbnailSizes () {
|
||||||
|
let maxWidth = 0
|
||||||
|
Object.entries(this.video.thumbnails).forEach(([thumbnailKey, thumbnail]) => {
|
||||||
|
if (thumbnail.width > maxWidth) maxWidth = thumbnail.width
|
||||||
|
})
|
||||||
|
|
||||||
|
// example:
|
||||||
|
// "(max-width: 640px) 100vw, 640px"
|
||||||
|
return `(max-width: ${maxWidth}px) 100vw, ${maxWidth}px`
|
||||||
|
},
|
||||||
|
thumbnailSrcset () {
|
||||||
|
// console.log('this.video', this.video)
|
||||||
|
|
||||||
|
// example:
|
||||||
|
// https://vumbnail.com/358629078.jpg 640w, https://vumbnail.com/358629078_large.jpg 640w, https://vumbnail.com/358629078_medium.jpg 200w, https://vumbnail.com/358629078_small.jpg 100w
|
||||||
|
return Object.entries(this.video.thumbnails).map(([thumbnailKey, thumbnail]) => {
|
||||||
|
// console.log('thumbnail', thumbnail)
|
||||||
|
return `${thumbnail.url} ${thumbnail.width}w`
|
||||||
|
}).join(', ')
|
||||||
|
},
|
||||||
|
pill () {
|
||||||
|
// if this video has a banchmark tag
|
||||||
|
// then pill is 'Benchmark'
|
||||||
|
if (this.video.tags.includes('benchmark')) return 'Benchmark'
|
||||||
|
|
||||||
|
// No pill
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
34
components/video/channel-credit.vue
Normal file
34
components/video/channel-credit.vue
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="video.channel.id !== myChannelId"
|
||||||
|
class="channel-credit"
|
||||||
|
>
|
||||||
|
<LinkButton
|
||||||
|
:href="`https://www.youtube.com/channel/${video.channel.id}`"
|
||||||
|
|
||||||
|
target="_blank"
|
||||||
|
>Subscribe to {{ video.channel.name }}</LinkButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import LinkButton from '~/components/link-button.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
LinkButton
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
video: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
myChannelId: 'UCB3jOb5QVjX7lYecvyCoTqQ'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
264
components/video/player.vue
Normal file
264
components/video/player.vue
Normal file
|
|
@ -0,0 +1,264 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:style="{
|
||||||
|
'left': '50%',
|
||||||
|
'right': '50%',
|
||||||
|
'margin-left': '-50vw',
|
||||||
|
'margin-right': '-50vw'
|
||||||
|
}"
|
||||||
|
class="video-canvas w-screen flex flex-col justify-center items-center bg-black"
|
||||||
|
>
|
||||||
|
<div class="ratio-wrapper w-full max-w-4xl">
|
||||||
|
<div class="relative overflow-hidden w-full pb-16/9">
|
||||||
|
<iframe
|
||||||
|
ref="frame"
|
||||||
|
:id="frameId"
|
||||||
|
:src="`https://www.youtube-nocookie.com/embed/${video.id}?enablejsapi=1&autoplay=1&modestbranding=1&playsinline=1`"
|
||||||
|
class="absolute h-full w-full"
|
||||||
|
frameborder="0"
|
||||||
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||||
|
allowfullscreen
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="hasTimestamps && hasPlayer"
|
||||||
|
class="video-timestamps w-full max-w-4xl"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref="timestamps-scroll-container"
|
||||||
|
class="featured-apps overflow-x-auto overflow-y-visible whitespace-no-wrap py-2 space-x-2"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="timestamp in timestamps"
|
||||||
|
:key="timestamp.time"
|
||||||
|
:ref="`timestamp-${timestamp.time}`"
|
||||||
|
:data-time="timestamp.time"
|
||||||
|
:class="[
|
||||||
|
'inline-block text-xs rounded-lg py-1 px-2',
|
||||||
|
'border-2 border-white focus:outline-none',
|
||||||
|
(activeTimestamp && activeTimestamp.time === timestamp.time) ? 'border-opacity-100 bg-darkest' : 'border-opacity-0 neumorphic-shadow-inner'
|
||||||
|
]"
|
||||||
|
:class-groups="{
|
||||||
|
shadow: 'neumorphic-shadow-inner'
|
||||||
|
}"
|
||||||
|
@click.stop="seekTo(timestamp.inSeconds); player.playVideo()"
|
||||||
|
>{{ timestamp.fullText }}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- activeTimestamp: {{ activeTimestamp }} -->
|
||||||
|
<!-- playerTime: {{ playerTime }} -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import LinkButton from '~/components/link-button.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
LinkButton
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
video: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
player: null,
|
||||||
|
playing: false,
|
||||||
|
progressInterval: null,
|
||||||
|
playerTime: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
frameId () {
|
||||||
|
return `youtube-player-${this.video.id}-${this._uid}`
|
||||||
|
},
|
||||||
|
timestamps () {
|
||||||
|
return this.video.timestamps.map( timestamp => {
|
||||||
|
const [ minutes, seconds ] = timestamp.time.split(':')
|
||||||
|
|
||||||
|
return {
|
||||||
|
...timestamp,
|
||||||
|
inSeconds: (minutes * 60) + Number(seconds)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
hasTimestamps () {
|
||||||
|
return this.timestamps.length > 0
|
||||||
|
},
|
||||||
|
hasPlayer () {
|
||||||
|
return this.player !== null
|
||||||
|
},
|
||||||
|
activeTimestamp () {
|
||||||
|
const currentTime = this.playerTime// / 100
|
||||||
|
|
||||||
|
const reversesTimestamps = [
|
||||||
|
...this.timestamps
|
||||||
|
]
|
||||||
|
|
||||||
|
// reversesTimestamps.reverse()
|
||||||
|
|
||||||
|
let foundTimestamp = null
|
||||||
|
|
||||||
|
for (const timestamp of reversesTimestamps) {
|
||||||
|
const hasStarted = currentTime > 1
|
||||||
|
const currentTimeisAfterPreviousTimestamp = (foundTimestamp !== null) ? currentTime > foundTimestamp.inSeconds : true
|
||||||
|
// const isPastCurrentTime = currentTime > timestamp.inSeconds
|
||||||
|
// const isBeforeCurrentTime = currentTime > timestamp.inSeconds
|
||||||
|
const currentTimeIsBeforeThisTimestamp = currentTime < timestamp.inSeconds
|
||||||
|
|
||||||
|
if (currentTimeisAfterPreviousTimestamp && currentTimeIsBeforeThisTimestamp) {
|
||||||
|
return foundTimestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
foundTimestamp = timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
// No active timestamp
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
// whenever question changes, this function will run
|
||||||
|
activeTimestamp: function (newTimestamp, oldTimestamp) {
|
||||||
|
|
||||||
|
// console.log('newTimestamp', newTimestamp)
|
||||||
|
// console.log('oldTimestamp', oldTimestamp)
|
||||||
|
|
||||||
|
const hasOldAndNewTimestamps = newTimestamp !== null && oldTimestamp !== null
|
||||||
|
|
||||||
|
if (!hasOldAndNewTimestamps) return
|
||||||
|
|
||||||
|
if (newTimestamp.inSeconds !== oldTimestamp.inSeconds) {
|
||||||
|
this.scrollRow ( newTimestamp )
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
video () {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.initializePlayer()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
// Set frame ID here so that it's the same when Youtube API looks for it
|
||||||
|
// this.frameId = `youtube-bg-${this._uid}`
|
||||||
|
|
||||||
|
this.initializePlayer()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
scrollRow ( timestamp ) {
|
||||||
|
|
||||||
|
// If timestamp button doesn't exist
|
||||||
|
// then stop
|
||||||
|
if (!this.$refs[`timestamp-${timestamp.time}`]) return
|
||||||
|
|
||||||
|
const timestampsScroller = this.$refs['timestamps-scroll-container']
|
||||||
|
const [ timestampButton ] = this.$refs[`timestamp-${timestamp.time}`]
|
||||||
|
|
||||||
|
// https://stackoverflow.com/a/63773123/1397641
|
||||||
|
const newScrollPosition = timestampButton.offsetLeft - timestampsScroller.offsetLeft
|
||||||
|
|
||||||
|
timestampsScroller.scroll({ left: newScrollPosition, behavior: 'smooth' })
|
||||||
|
},
|
||||||
|
seekTo (timestampInSeconds) {
|
||||||
|
this.player.seekTo(timestampInSeconds)
|
||||||
|
},
|
||||||
|
async initializePlayer () {
|
||||||
|
// console.log('Youtube Embed API Ready')
|
||||||
|
|
||||||
|
// Clear player
|
||||||
|
this.player = null
|
||||||
|
|
||||||
|
// Clear tprogession interval
|
||||||
|
clearInterval(this.progressInterval)
|
||||||
|
|
||||||
|
// If there are no timestamps
|
||||||
|
// then stop
|
||||||
|
if (!this.hasTimestamps) return
|
||||||
|
|
||||||
|
if (typeof YT === 'undefined') {
|
||||||
|
await this.initializeApi()
|
||||||
|
}
|
||||||
|
|
||||||
|
const stateHandlers = {
|
||||||
|
// unstarted
|
||||||
|
'-1': () => null,
|
||||||
|
// ended
|
||||||
|
'0': () => null,
|
||||||
|
// playing
|
||||||
|
'1': this.onPlayerPlaying,
|
||||||
|
// paused
|
||||||
|
'2': this.onPlayerPaused,
|
||||||
|
// buffering
|
||||||
|
'3': () => null,
|
||||||
|
// video cued
|
||||||
|
'4': () => null,
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log('frame', this.$refs['frame'])
|
||||||
|
// console.log('frame id', this.$refs['frame'].id)
|
||||||
|
|
||||||
|
this.player = new YT.Player(this.$refs['frame'].id, {
|
||||||
|
events: {
|
||||||
|
'onReady': this.onPlayerReady,
|
||||||
|
'onStateChange': event => {
|
||||||
|
// console.log('state changed', event)
|
||||||
|
|
||||||
|
const stateHandler = stateHandlers[String(event.data)]
|
||||||
|
// console.log('stateHandler', stateHandler)
|
||||||
|
stateHandler(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
initializeApi () {
|
||||||
|
return new Promise( resolve => {
|
||||||
|
const tag = document.createElement('script')
|
||||||
|
tag.id = `youtube-api-script-${this._uid}`
|
||||||
|
tag.src = 'https://www.youtube.com/iframe_api'
|
||||||
|
|
||||||
|
const firstScriptTag = document.getElementsByTagName('script')[0]
|
||||||
|
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag)
|
||||||
|
|
||||||
|
|
||||||
|
window.onYouTubeIframeAPIReady = resolve
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
onPlayerPlaying () {
|
||||||
|
console.log('Player playing')
|
||||||
|
this.playing = true
|
||||||
|
|
||||||
|
this.progressInterval = setInterval(() => {
|
||||||
|
// console.log('this.player.getCurrentTime()', this.player.getCurrentTime())
|
||||||
|
|
||||||
|
// If player is empty
|
||||||
|
// then stop
|
||||||
|
if (this.player === null) {
|
||||||
|
clearInterval(this.progressInterval)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log('this.player', this.player.hasOwnProperty('getCurrentTime'))
|
||||||
|
|
||||||
|
this.playerTime = this.player.getCurrentTime()
|
||||||
|
}, 500)
|
||||||
|
},
|
||||||
|
onPlayerPaused () {
|
||||||
|
console.log('Player paused')
|
||||||
|
this.playing = false
|
||||||
|
|
||||||
|
clearInterval(this.progressInterval)
|
||||||
|
},
|
||||||
|
onPlayerReady (event) {
|
||||||
|
console.log('Player is ready', this.player)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
109
components/video/row.vue
Normal file
109
components/video/row.vue
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
<template>
|
||||||
|
<div class="video-row relative w-full">
|
||||||
|
<div
|
||||||
|
ref="row"
|
||||||
|
:style="{
|
||||||
|
scrollSnapType: 'x mandatory'
|
||||||
|
}"
|
||||||
|
class="video-row-contents flex overflow-x-auto whitespace-no-wrap py-2 space-x-6"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
v-for="video in videos"
|
||||||
|
:is="cardType ( video )"
|
||||||
|
:key="video.slug"
|
||||||
|
:video="video"
|
||||||
|
:style="{
|
||||||
|
maxWidth: `${cardWidth}px`,
|
||||||
|
flexBasis: `${cardWidth}px`,
|
||||||
|
scrollSnapAlign: 'start'
|
||||||
|
}"
|
||||||
|
:class="[
|
||||||
|
'w-full flex-shrink-0 flex-grow-0 border-2 border-transparent rounded-2xl overflow-hidden',
|
||||||
|
(activeVideoId === video.id) ? 'border-white' : null
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
:style="{
|
||||||
|
top: '50%',
|
||||||
|
}"
|
||||||
|
class="absolute left-0 h-10 w-10 flex justify-center items-center transform -translate-y-1/2 -translate-x-1/2 bg-darker rounded-full"
|
||||||
|
@click="scrollRow(cardWidth * -1)"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="h-5 w-5 text-gray-400"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
style="transform: scaleX(-1);"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
:style="{
|
||||||
|
top: '50%',
|
||||||
|
}"
|
||||||
|
class="absolute right-0 h-10 w-10 flex justify-center items-center transform -translate-y-1/2 translate-x-1/2 bg-darker rounded-full"
|
||||||
|
@click="scrollRow(cardWidth)"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="h-5 w-5 text-gray-400"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import SubmitCard from '~/components/video/submit-card.vue'
|
||||||
|
import VideoCard from '~/components/video/card.vue'
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
SubmitCard,
|
||||||
|
VideoCard
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
videos: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
activeVideoId: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
cardWidth: {
|
||||||
|
type: Number,
|
||||||
|
default: 325
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
scrollRow ( pixels ) {
|
||||||
|
this.$refs['row'].scrollBy({ left: pixels, behavior: 'smooth' })
|
||||||
|
},
|
||||||
|
isSubmitCard ( video ) {
|
||||||
|
return video.endpoint.includes('https://docs.google.com/forms')
|
||||||
|
},
|
||||||
|
cardType ( video ) {
|
||||||
|
if (this.isSubmitCard(video)) return SubmitCard
|
||||||
|
|
||||||
|
return VideoCard
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
69
components/video/submit-card.vue
Normal file
69
components/video/submit-card.vue
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
<template>
|
||||||
|
|
||||||
|
<div class="video-card">
|
||||||
|
<a
|
||||||
|
:href="video.endpoint"
|
||||||
|
class=""
|
||||||
|
>
|
||||||
|
<div class="video-card-container relative overflow-hidden bg-white">
|
||||||
|
<div class="video-card-image ratio-wrapper">
|
||||||
|
<div class="relative overflow-hidden w-full pb-16/9">
|
||||||
|
<!-- <img
|
||||||
|
:srcset="thumbnailSrcset"
|
||||||
|
:sizes="thumbnailSizes"
|
||||||
|
:src="video.thumbnails.default.url"
|
||||||
|
:alt="video.name"
|
||||||
|
class="absolute h-full w-full object-cover"
|
||||||
|
> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
:style="{
|
||||||
|
'--gradient-from-color': 'rgba(0, 0, 0, 1)',
|
||||||
|
'--gradient-to-color': 'rgba(0, 0, 0, 0.7)'
|
||||||
|
}"
|
||||||
|
class="video-card-overlay absolute inset-0 flex justify-between items-start bg-gradient-to-tr from-black to-transparent p-4"
|
||||||
|
>
|
||||||
|
<div class="plus-circle w-8 h-8 bg-white-2 flex justify-center items-center outline-0 rounded-full ease">
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
style="width:18px;height:18px;"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M11 11v-11h1v11h11v1h-11v11h-1v-11h-11v-1h11z"
|
||||||
|
/>
|
||||||
|
<!-- Plus Icon: "M11 11v-11h1v11h11v1h-11v11h-1v-11h-11v-1h11z" -->
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Video Text Content -->
|
||||||
|
<div class="video-card-content absolute inset-0 flex items-end py-4 px-6">
|
||||||
|
<div class="w-full text-sm text-left whitespace-normal">Submit Video</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import { getVideoEndpoint } from '~/helpers/app-derived.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
video: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// computed: {},
|
||||||
|
methods: {
|
||||||
|
getVideoEndpoint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
@ -3,9 +3,20 @@
|
||||||
export function getAppEndpoint ( app ) {
|
export function getAppEndpoint ( app ) {
|
||||||
// console.log('app', app)
|
// console.log('app', app)
|
||||||
|
|
||||||
|
if(app.category !== Object(app.category)) {
|
||||||
|
console.warn('app has no categories', app)
|
||||||
|
}
|
||||||
|
|
||||||
if (app.category.slug === 'homebrew') return `/formula/${app.slug}`
|
if (app.category.slug === 'homebrew') return `/formula/${app.slug}`
|
||||||
|
|
||||||
if (app.category.slug === 'games') return `/game/${app.slug}`
|
if (app.category.slug === 'games') return `/game/${app.slug}`
|
||||||
|
|
||||||
return `/app/${app.slug}`
|
return `/app/${app.slug}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getVideoEndpoint ( video ) {
|
||||||
|
|
||||||
|
return `/tv/${video.slug}`
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -84,8 +84,8 @@ class MakeHomebrewList {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('formulaData', formulaData)
|
// console.log('formulaData', formulaData)
|
||||||
console.log('formulae', formulae)
|
// console.log('formulae', formulae)
|
||||||
|
|
||||||
// Check the official list first since it's data is newer and more frequently updated
|
// Check the official list first since it's data is newer and more frequently updated
|
||||||
const hasStableFormula = (formulaData.bottle.stable !== undefined)
|
const hasStableFormula = (formulaData.bottle.stable !== undefined)
|
||||||
|
|
|
||||||
161
helpers/build-video-list.js
Normal file
161
helpers/build-video-list.js
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
|
||||||
|
import slugify from 'slugify'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
import { byTimeThenNull } from './sort-list.js'
|
||||||
|
import { getVideoEndpoint } from './app-derived.js'
|
||||||
|
import parseGithubDate from './parse-github-date'
|
||||||
|
|
||||||
|
export function matchesWholeWord (needle, haystack) {
|
||||||
|
return new RegExp('\\b' + needle + '\\b').test(haystack)
|
||||||
|
}
|
||||||
|
|
||||||
|
const videoFeaturesApp = function (app, video) {
|
||||||
|
const appFuzzyName = app.name.toLowerCase()
|
||||||
|
if (video.title.toLowerCase().includes(appFuzzyName)) return true
|
||||||
|
|
||||||
|
const appIsInTimestamps = video.timestamps.map( timestamp => timestamp.fullText.toLowerCase()).includes(appFuzzyName)
|
||||||
|
|
||||||
|
if (appIsInTimestamps) return true
|
||||||
|
|
||||||
|
if (matchesWholeWord(appFuzzyName, video.description.toLowerCase())) return true
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const generateVideoTags = function ( video ) {
|
||||||
|
const tags = {
|
||||||
|
'benchmark': {
|
||||||
|
relatedWords: [
|
||||||
|
'benchmarks',
|
||||||
|
'comparison',
|
||||||
|
'speed test',
|
||||||
|
'bench mark',
|
||||||
|
'bench marks'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'performance': {
|
||||||
|
relatedWords: [
|
||||||
|
'speed'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const videoTags = new Set()
|
||||||
|
|
||||||
|
video.tags.forEach( tag => {
|
||||||
|
videoTags.add(tag)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Match tags against video titles and descriptions
|
||||||
|
for (const tagKey in tags) {
|
||||||
|
|
||||||
|
// Skip if this video already has this tag
|
||||||
|
// then skip it
|
||||||
|
if (videoTags.has(tagKey)) continue
|
||||||
|
|
||||||
|
const matchingWords = [
|
||||||
|
tagKey,
|
||||||
|
...tags[tagKey].relatedWords
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const tagWord of matchingWords) {
|
||||||
|
|
||||||
|
// Skip if this video already has this tag
|
||||||
|
// then stop this loop
|
||||||
|
if (videoTags.has(tagKey)) break
|
||||||
|
|
||||||
|
// Check title
|
||||||
|
if (matchesWholeWord(tagWord.toLowerCase(), video.title.toLowerCase())) {
|
||||||
|
videoTags.add(tagKey)
|
||||||
|
|
||||||
|
// console.log(video.title, 'has', tagKey, 'tag')
|
||||||
|
|
||||||
|
// We're done since the tag matched for the title
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check description
|
||||||
|
if (matchesWholeWord(tagWord.toLowerCase(), video.description.toLowerCase())) {
|
||||||
|
videoTags.add(tagKey)
|
||||||
|
|
||||||
|
// console.log(video.title, 'has', tagKey, 'tag')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return videoTags
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function ( applist ) {
|
||||||
|
|
||||||
|
// Fetch Commits
|
||||||
|
const response = await axios.get(process.env.VIDEO_SOURCE)
|
||||||
|
// Extract commit from response data
|
||||||
|
const fetchedVideos = response.data
|
||||||
|
|
||||||
|
// console.log('fetchedVideos', fetchedVideos)
|
||||||
|
|
||||||
|
const videos = []
|
||||||
|
|
||||||
|
for (const videoId in fetchedVideos) {
|
||||||
|
|
||||||
|
// Skip private videos
|
||||||
|
if (fetchedVideos[videoId].title === 'Private video') continue
|
||||||
|
|
||||||
|
// Skip deleted videos
|
||||||
|
if (fetchedVideos[videoId].title === 'Deleted video') continue
|
||||||
|
|
||||||
|
// Build video slug
|
||||||
|
const slug = slugify(`${fetchedVideos[videoId].title}-i-${videoId}`, {
|
||||||
|
lower: true,
|
||||||
|
strict: true
|
||||||
|
})
|
||||||
|
|
||||||
|
const apps = []
|
||||||
|
// Generate new tag set based on api data
|
||||||
|
const tags = generateVideoTags(fetchedVideos[videoId])
|
||||||
|
|
||||||
|
for ( const app of applist ) {
|
||||||
|
if (videoFeaturesApp(app, fetchedVideos[videoId])) {
|
||||||
|
apps.push(app.slug)
|
||||||
|
|
||||||
|
tags.add(app.category.slug)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log('fetchedVideos[videoId].rawData.snippet', fetchedVideos[videoId].rawData.snippet)
|
||||||
|
|
||||||
|
const lastUpdated = {
|
||||||
|
raw: fetchedVideos[videoId].rawData.snippet.publishedAt,
|
||||||
|
timestamp: parseGithubDate(fetchedVideos[videoId].rawData.snippet.publishedAt).timestamp,
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log('fetchedVideos[videoId].thumbnails', fetchedVideos[videoId].thumbnails)
|
||||||
|
|
||||||
|
videos.push({
|
||||||
|
name: fetchedVideos[videoId].title,
|
||||||
|
id: videoId,
|
||||||
|
lastUpdated,
|
||||||
|
apps,
|
||||||
|
slug,
|
||||||
|
channel:{
|
||||||
|
name: fetchedVideos[videoId].rawData.snippet.channelTitle,
|
||||||
|
id: fetchedVideos[videoId].rawData.snippet.channelId
|
||||||
|
},
|
||||||
|
// Convert tags set into array
|
||||||
|
tags: Array.from(tags),
|
||||||
|
timestamps: fetchedVideos[videoId].timestamps,
|
||||||
|
thumbnails: fetchedVideos[videoId].rawData.snippet.thumbnails,
|
||||||
|
endpoint: getVideoEndpoint({
|
||||||
|
slug
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log('videos', videos)
|
||||||
|
|
||||||
|
// publishedAt
|
||||||
|
|
||||||
|
return videos.sort(byTimeThenNull)
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,14 @@ import gameList from '~/static/game-list.json'
|
||||||
import homebrewList from '~/static/homebrew-list.json'
|
import homebrewList from '~/static/homebrew-list.json'
|
||||||
|
|
||||||
import { byTimeThenNull } from '~/helpers/sort-list.js'
|
import { byTimeThenNull } from '~/helpers/sort-list.js'
|
||||||
|
import { videosRelatedToApp } from '~/helpers/related.js'
|
||||||
|
import { getAppEndpoint } from '~/helpers/app-derived.js'
|
||||||
|
|
||||||
|
|
||||||
|
export const allVideoAppsList = [
|
||||||
|
...appList.sort(byTimeThenNull),
|
||||||
|
...gameList,
|
||||||
|
]
|
||||||
|
|
||||||
export const sortedAppList = appList.sort(byTimeThenNull)
|
export const sortedAppList = appList.sort(byTimeThenNull)
|
||||||
|
|
||||||
|
|
@ -11,3 +19,54 @@ export const allList = [
|
||||||
...homebrewList,
|
...homebrewList,
|
||||||
...gameList,
|
...gameList,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
export function makeAppSearchLinks (app) {
|
||||||
|
|
||||||
|
const videos = videosRelatedToApp(app)
|
||||||
|
|
||||||
|
// If there are no videos
|
||||||
|
// then skip
|
||||||
|
if (videos.length === 0) return []
|
||||||
|
|
||||||
|
const searchLinks = []
|
||||||
|
|
||||||
|
const appBenchmarksUrl = `${getAppEndpoint(app)}/benchmarks`
|
||||||
|
|
||||||
|
let hasPerformanceVideo = false
|
||||||
|
|
||||||
|
for (const video of videos) {
|
||||||
|
// If there are no video tags
|
||||||
|
// then skip
|
||||||
|
if (video.tags.length === 0) continue
|
||||||
|
|
||||||
|
// If there's any benchmark video then add
|
||||||
|
if (video.tags.includes('benchmark')) {
|
||||||
|
// Add a benchmark link
|
||||||
|
searchLinks.push({
|
||||||
|
href: appBenchmarksUrl,
|
||||||
|
label: 'Benchmarks'
|
||||||
|
})
|
||||||
|
|
||||||
|
// then stop looking
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (video.tags.includes('performance')) {
|
||||||
|
hasPerformanceVideo = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there was no bechmark video found
|
||||||
|
// but there was a performance video found
|
||||||
|
// then push Performance link
|
||||||
|
if (searchLinks.length === 0 && hasPerformanceVideo) {
|
||||||
|
// Add a performance link
|
||||||
|
searchLinks.push({
|
||||||
|
href: appBenchmarksUrl,
|
||||||
|
label: 'Performance'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return searchLinks
|
||||||
|
}
|
||||||
|
|
|
||||||
57
helpers/related.js
Normal file
57
helpers/related.js
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { allVideoAppsList } from '~/helpers/get-list.js'
|
||||||
|
import videoList from '~/static/video-list.json'
|
||||||
|
|
||||||
|
export function matchesWholeWord (needle, haystack) {
|
||||||
|
return new RegExp('\\b' + needle + '\\b').test(haystack)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function appsRelatedToVideo ( video ) {
|
||||||
|
const relatedApps = []
|
||||||
|
|
||||||
|
// Find the apps listed in this video
|
||||||
|
for (const app of allVideoAppsList) {
|
||||||
|
// Skip this app if it's not listed in the videos apps
|
||||||
|
if (!video.apps.includes(app.slug)) continue
|
||||||
|
|
||||||
|
// Add this app to our featured app list
|
||||||
|
relatedApps.push(app)
|
||||||
|
}
|
||||||
|
|
||||||
|
return relatedApps
|
||||||
|
}
|
||||||
|
|
||||||
|
export function videosRelatedToVideo ( video ) {
|
||||||
|
const relatedVideos = {}
|
||||||
|
|
||||||
|
const featuredApps = appsRelatedToVideo( video )
|
||||||
|
|
||||||
|
// Find other videos that also feature this video's app
|
||||||
|
for (const otherVideo of videoList) {
|
||||||
|
for (const app of featuredApps) {
|
||||||
|
// Skip if this app is not in the other video's apps
|
||||||
|
if (!otherVideo.apps.includes(app.slug)) continue
|
||||||
|
|
||||||
|
// Skip if the other video is, in fact, this video
|
||||||
|
if (otherVideo.slug === video.slug) continue
|
||||||
|
|
||||||
|
// Add this video to our related videos list
|
||||||
|
relatedVideos[otherVideo.id] = otherVideo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.values(relatedVideos)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function videosRelatedToApp ( app ) {
|
||||||
|
const relatedVideos = {}
|
||||||
|
|
||||||
|
// Find other videos that also feature this video's app
|
||||||
|
for (const video of videoList) {
|
||||||
|
if (!video.apps.includes(app.slug)) continue
|
||||||
|
|
||||||
|
relatedVideos[video.id] = video
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.values(relatedVideos)
|
||||||
|
}
|
||||||
106
nuxt.config.js
106
nuxt.config.js
|
|
@ -5,9 +5,10 @@ import pkg from './package'
|
||||||
import buildAppList from './helpers/build-app-list.js'
|
import buildAppList from './helpers/build-app-list.js'
|
||||||
import buildGamesList from './helpers/build-game-list.js'
|
import buildGamesList from './helpers/build-game-list.js'
|
||||||
import buildHomebrewList from './helpers/build-homebrew-list.js'
|
import buildHomebrewList from './helpers/build-homebrew-list.js'
|
||||||
|
import buildVideoList from './helpers/build-video-list.js'
|
||||||
|
|
||||||
import { categories } from './helpers/categories.js'
|
import { categories } from './helpers/categories.js'
|
||||||
import { getAppEndpoint } from './helpers/app-derived.js'
|
import { getAppEndpoint, getVideoEndpoint } from './helpers/app-derived.js'
|
||||||
|
|
||||||
|
|
||||||
const listsOptions = [
|
const listsOptions = [
|
||||||
|
|
@ -25,40 +26,62 @@ const listsOptions = [
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const videoListOptions = {
|
||||||
|
buildMethod: buildVideoList,
|
||||||
|
path: '/static/video-list.json',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const saveList = async function ( list, buildArgs = null ) {
|
||||||
|
const methodName = `Building ${list.path}`
|
||||||
|
console.time(methodName)
|
||||||
|
|
||||||
|
// Run the build method
|
||||||
|
const builtList = await list.buildMethod(buildArgs)
|
||||||
|
|
||||||
|
// Make the relative path for our new JSON file
|
||||||
|
const listFullPath = `.${list.path}`
|
||||||
|
|
||||||
|
// console.log('listFullPath', listFullPath)
|
||||||
|
|
||||||
|
// Write the list to JSON
|
||||||
|
await fs.writeFile(listFullPath, JSON.stringify(builtList))
|
||||||
|
|
||||||
|
// Read back the JSON we just wrote to ensure it exists
|
||||||
|
const savedListJSON = await fs.readFile(listFullPath, 'utf-8')
|
||||||
|
|
||||||
|
// console.log('savedListJSON', savedListJSON)
|
||||||
|
|
||||||
|
const savedList = JSON.parse(savedListJSON)
|
||||||
|
|
||||||
|
|
||||||
|
console.timeEnd(methodName)
|
||||||
|
|
||||||
|
// Import the created JSON File
|
||||||
|
return savedList
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const storeAppLists = async function (builder) {
|
const storeAppLists = async function (builder) {
|
||||||
|
|
||||||
console.log('Build Lists started')
|
console.log('Build Lists started')
|
||||||
|
|
||||||
const savedLists = await Promise.all(listsOptions.map(async list => {
|
const savedLists = await Promise.all(listsOptions.map(saveList))
|
||||||
|
// Build and save list of videos based on app lists
|
||||||
|
.then(async lists => {
|
||||||
|
const [
|
||||||
|
appList,
|
||||||
|
gameList
|
||||||
|
] = lists
|
||||||
|
|
||||||
const methodName = `Building ${list.path}`
|
// Build a video app list with apps and games only
|
||||||
console.time(methodName)
|
const videoAppList = [
|
||||||
|
...appList,
|
||||||
|
...gameList
|
||||||
|
].flat(1)
|
||||||
|
|
||||||
// Run the build method
|
return await saveList(videoListOptions, videoAppList)
|
||||||
const builtList = await list.buildMethod()
|
})
|
||||||
|
|
||||||
// Make the relative path for our new JSON file
|
|
||||||
const listFullPath = `.${list.path}`
|
|
||||||
|
|
||||||
// console.log('listFullPath', listFullPath)
|
|
||||||
|
|
||||||
// Write the list to JSON
|
|
||||||
await fs.writeFile(listFullPath, JSON.stringify(builtList))
|
|
||||||
|
|
||||||
// Read back the JSON we just wrote to ensure it exists
|
|
||||||
const savedListJSON = await fs.readFile(listFullPath, 'utf-8')
|
|
||||||
|
|
||||||
// console.log('savedListJSON', savedListJSON)
|
|
||||||
|
|
||||||
const savedList = JSON.parse(savedListJSON)
|
|
||||||
|
|
||||||
|
|
||||||
console.timeEnd(methodName)
|
|
||||||
|
|
||||||
// Import the created JSON File
|
|
||||||
return savedList
|
|
||||||
}))
|
|
||||||
|
|
||||||
console.log('Build Lists finished')
|
console.log('Build Lists finished')
|
||||||
|
|
||||||
|
|
@ -94,7 +117,12 @@ export default {
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
routes() {
|
routes() {
|
||||||
return Promise.all(listsOptions.map(async list => {
|
return Promise.all([
|
||||||
|
...listsOptions,
|
||||||
|
videoListOptions
|
||||||
|
].map(async list => {
|
||||||
|
// Read saved lists
|
||||||
|
|
||||||
const methodName = `Reading ${list.path}`
|
const methodName = `Reading ${list.path}`
|
||||||
console.time(methodName)
|
console.time(methodName)
|
||||||
|
|
||||||
|
|
@ -117,13 +145,27 @@ export default {
|
||||||
const [
|
const [
|
||||||
appRoutes,
|
appRoutes,
|
||||||
gameRoutes,
|
gameRoutes,
|
||||||
|
videoRoutes,
|
||||||
homebrewRoutes
|
homebrewRoutes
|
||||||
] = lists.map((list, listI) => {
|
] = lists.map((list, listI) => {
|
||||||
return list.map( app => {
|
return list.map( app => {
|
||||||
|
|
||||||
|
const isVideo = (app.category === undefined)
|
||||||
|
|
||||||
|
if (isVideo) {
|
||||||
|
return getVideoEndpoint(app)
|
||||||
|
}
|
||||||
|
|
||||||
return getAppEndpoint(app)
|
return getAppEndpoint(app)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Build routes for app types that support benchmark endpoints
|
||||||
|
const benchmarkRoutes = [
|
||||||
|
...appRoutes,
|
||||||
|
// ...gameRoutes,
|
||||||
|
].flat(1).map( route => `${route}/benchmarks`)
|
||||||
|
|
||||||
// console.log('homebrewRoutes', homebrewRoutes)
|
// console.log('homebrewRoutes', homebrewRoutes)
|
||||||
|
|
||||||
const categoryRoutes = Object.keys(categories).map( slug => ({
|
const categoryRoutes = Object.keys(categories).map( slug => ({
|
||||||
|
|
@ -135,7 +177,11 @@ export default {
|
||||||
...appRoutes,
|
...appRoutes,
|
||||||
...gameRoutes,
|
...gameRoutes,
|
||||||
...homebrewRoutes,
|
...homebrewRoutes,
|
||||||
...categoryRoutes
|
|
||||||
|
// Non-app routes
|
||||||
|
...videoRoutes,
|
||||||
|
...categoryRoutes,
|
||||||
|
...benchmarkRoutes
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
5
package-lock.json
generated
5
package-lock.json
generated
|
|
@ -7522,6 +7522,11 @@
|
||||||
"launch-editor": "^2.2.1"
|
"launch-editor": "^2.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"lazysizes": {
|
||||||
|
"version": "5.3.0-beta1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lazysizes/-/lazysizes-5.3.0-beta1.tgz",
|
||||||
|
"integrity": "sha512-4ZEoV4UXvz5L96XBuaFphGlq8BVi78kIA4kL3ls2qSYMEgGBdXMgsh8u+bTVjbdwbXfzXtWc71OH4EfEEPVZoA=="
|
||||||
|
},
|
||||||
"levn": {
|
"levn": {
|
||||||
"version": "0.3.0",
|
"version": "0.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
"@nuxtjs/sitemap": "^2.4.0",
|
"@nuxtjs/sitemap": "^2.4.0",
|
||||||
"axios": "^0.21.0",
|
"axios": "^0.21.0",
|
||||||
"cross-env": "^5.2.0",
|
"cross-env": "^5.2.0",
|
||||||
|
"lazysizes": "^5.3.0-beta1",
|
||||||
"markdown-it": "^11.0.1",
|
"markdown-it": "^11.0.1",
|
||||||
"marked": "^1.2.7",
|
"marked": "^1.2.7",
|
||||||
"node-html-parser": "^2.0.0",
|
"node-html-parser": "^2.0.0",
|
||||||
|
|
|
||||||
265
pages/app/_slug/benchmarks.vue
Normal file
265
pages/app/_slug/benchmarks.vue
Normal file
|
|
@ -0,0 +1,265 @@
|
||||||
|
<template>
|
||||||
|
<section class="container pb-16">
|
||||||
|
<div class="flex flex-col items-center text-center space-y-8">
|
||||||
|
<template
|
||||||
|
v-if="video"
|
||||||
|
>
|
||||||
|
<VideoPlayer
|
||||||
|
:video="video"
|
||||||
|
class="pt-16"
|
||||||
|
/>
|
||||||
|
<ChannelCredit
|
||||||
|
:video="video"
|
||||||
|
class="flex w-full justify-start md:px-10"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div
|
||||||
|
:style="{
|
||||||
|
'left': '50%',
|
||||||
|
'right': '50%',
|
||||||
|
'margin-left': '-50vw',
|
||||||
|
'margin-right': '-50vw'
|
||||||
|
}"
|
||||||
|
class="video-canvas w-screen flex justify-center bg-black pt-16"
|
||||||
|
>
|
||||||
|
<div class="ratio-wrapper w-full max-w-4xl">
|
||||||
|
<div class="relative overflow-hidden w-full pb-16/9">
|
||||||
|
<div class="absolute h-full w-full flex justify-center items-center">
|
||||||
|
<div class="message text-4xl md:text-6xl font-hairline leading-tight text-center">No videos yet</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- <h1 class="title text-sm md:text-3xl font-semibold">
|
||||||
|
{{ video.name }}
|
||||||
|
</h1> -->
|
||||||
|
|
||||||
|
<div class="related-videos w-full max-w-4xl">
|
||||||
|
<h2 class="subtitle text-xl md:text-2xl font-bold mb-3">
|
||||||
|
Benchmark Videos
|
||||||
|
</h2>
|
||||||
|
<!-- <pre class="text-left">{{ benchmarkVideos }}</pre> -->
|
||||||
|
<VideoRow
|
||||||
|
:videos="benchmarkVideos"
|
||||||
|
:active-video-id="activeVideoId"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="performanceVideos.length !== 0"
|
||||||
|
class="performance-videos w-full max-w-4xl"
|
||||||
|
>
|
||||||
|
<h2 class="subtitle text-xl md:text-2xl font-bold mb-3">
|
||||||
|
Performance Videos
|
||||||
|
</h2>
|
||||||
|
<!-- <pre class="text-left">{{ performanceVideos }}</pre> -->
|
||||||
|
<VideoRow
|
||||||
|
:videos="performanceVideos"
|
||||||
|
:active-video-id="activeVideoId"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="moreVideos.length !== 0"
|
||||||
|
class="related-videos w-full max-w-4xl"
|
||||||
|
>
|
||||||
|
<h2 class="subtitle text-xl md:text-2xl font-bold mb-3">
|
||||||
|
More Videos
|
||||||
|
</h2>
|
||||||
|
<!-- <pre class="text-left">{{ relatedVideos }}</pre> -->
|
||||||
|
<VideoRow
|
||||||
|
:videos="moreVideos"
|
||||||
|
:active-video-id="activeVideoId"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- video: {{ video }} -->
|
||||||
|
|
||||||
|
<!-- <div class="links space-y-6 sm:space-x-6 mb-8">
|
||||||
|
<LinkButton
|
||||||
|
v-for="(link, i) in app.relatedLinks"
|
||||||
|
:key="i"
|
||||||
|
:href="link.href"
|
||||||
|
target="_blank"
|
||||||
|
class=""
|
||||||
|
>{{ (i === 0) ? 'View' : link.label }}</LinkButton>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import LinkButton from '~/components/link-button.vue'
|
||||||
|
import EmailSubscribe from '~/components/email-subscribe.vue'
|
||||||
|
import VideoRow from '~/components/video/row.vue'
|
||||||
|
import VideoPlayer from '~/components/video/player.vue'
|
||||||
|
import ChannelCredit from '~/components/video/channel-credit.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
LinkButton,
|
||||||
|
EmailSubscribe,
|
||||||
|
VideoRow,
|
||||||
|
VideoPlayer,
|
||||||
|
ChannelCredit
|
||||||
|
},
|
||||||
|
async asyncData ({ params: { slug } }) {
|
||||||
|
|
||||||
|
const { allVideoAppsList } = await import('~/helpers/get-list.js')
|
||||||
|
// const { default: videoList } = await import('~/static/video-list.json')
|
||||||
|
|
||||||
|
const { videosRelatedToApp } = await import('~/helpers/related.js')
|
||||||
|
|
||||||
|
const app = allVideoAppsList.find(app => (app.slug === slug))
|
||||||
|
|
||||||
|
const submitVideoCard = {
|
||||||
|
endpoint: `https://docs.google.com/forms/d/e/1FAIpQLSeEVGM9vE7VcfLMy6fJkfU70X2VZ60rHDyhDQLtnAN4nso0WA/viewform?usp=pp_url&entry.1018125313=${app.name}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// const featuredApps = []
|
||||||
|
|
||||||
|
const relatedVideos = videosRelatedToApp( app ).map(video => {
|
||||||
|
// console.log('video', video)
|
||||||
|
return {
|
||||||
|
...video,
|
||||||
|
// endpoint: `#${video.id}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
app,
|
||||||
|
allVideos: relatedVideos,
|
||||||
|
submitVideoCard
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
activeVideoIndex: 0,
|
||||||
|
benchmarkVideos: [],
|
||||||
|
performanceVideos: [],
|
||||||
|
moreVideos: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
video () {
|
||||||
|
return this.allVideos[this.activeVideoIndex]
|
||||||
|
},
|
||||||
|
title () {
|
||||||
|
return `${this.app.name} Benchmarks for Apple Silicon - Does It ARM`
|
||||||
|
},
|
||||||
|
description () {
|
||||||
|
return `Apple Silicon benchmark, performance, and support videos for ${this.app.name}`
|
||||||
|
},
|
||||||
|
activeVideoId () {
|
||||||
|
return (this.video === Object(this.video)) ? this.video.id : null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
|
||||||
|
const nonBenchmarkVideos = []
|
||||||
|
|
||||||
|
// console.log('benchmarkVideos.length', this.benchmarkVideos.length)
|
||||||
|
// console.log('performanceVideos.length', this.performanceVideos.length)
|
||||||
|
// console.log('moreVideos.length', this.moreVideos.length)
|
||||||
|
|
||||||
|
// Move benchmark videos out of related videos
|
||||||
|
this.allVideos.forEach((video, index) => {
|
||||||
|
// console.log('video.name', video.name)
|
||||||
|
// console.log('video.tags', video.tags)
|
||||||
|
|
||||||
|
if (!video.tags.includes('benchmark')) {
|
||||||
|
nonBenchmarkVideos.push(video)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Add to benchmark videos
|
||||||
|
this.benchmarkVideos.push(video)
|
||||||
|
})
|
||||||
|
|
||||||
|
// console.log('Added benchmark videos')
|
||||||
|
// console.log('benchmarkVideos.length', this.benchmarkVideos.length)
|
||||||
|
// console.log('performanceVideos.length', this.performanceVideos.length)
|
||||||
|
// console.log('moreVideos.length', this.moreVideos.length)
|
||||||
|
|
||||||
|
// Move performance videos out of related videos
|
||||||
|
nonBenchmarkVideos.forEach((video, index) => {
|
||||||
|
|
||||||
|
if (!video.tags.includes('performance')) {
|
||||||
|
this.moreVideos.push(video)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to benchmark videos
|
||||||
|
this.performanceVideos.push(video)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Append submit card to end
|
||||||
|
this.benchmarkVideos.push(this.submitVideoCard)
|
||||||
|
|
||||||
|
|
||||||
|
// console.log('Added performance videos')
|
||||||
|
// console.log('benchmarkVideos.length', this.benchmarkVideos.length)
|
||||||
|
// console.log('performanceVideos.length', this.performanceVideos.length)
|
||||||
|
// console.log('moreVideos.length', this.moreVideos.length)
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
window.onhashchange = this.loadVideoFromHash
|
||||||
|
|
||||||
|
if (location.hash.length !== 0) this.loadVideoFromHash()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
loadVideoFromHash () {
|
||||||
|
// console.log('location.hash', location.hash)
|
||||||
|
|
||||||
|
// Separate the video id from our window hash
|
||||||
|
const hashId = location.hash.split('#')[1]
|
||||||
|
|
||||||
|
// Find the index of the video with the matching hash
|
||||||
|
const newVideoIndex = this.allVideos.findIndex(video => {
|
||||||
|
return video.id === hashId
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('newVideoIndex', newVideoIndex)
|
||||||
|
|
||||||
|
// Load in the index to load out video
|
||||||
|
this.activeVideoIndex = newVideoIndex
|
||||||
|
|
||||||
|
window.scroll({ top: 0, behavior: 'smooth' })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
head() {
|
||||||
|
return {
|
||||||
|
title: this.title,
|
||||||
|
meta: [
|
||||||
|
// hid is used as unique identifier. Do not use `vmid` for it as it will not work
|
||||||
|
{
|
||||||
|
'hid': 'description',
|
||||||
|
'name': 'description',
|
||||||
|
'content': this.description
|
||||||
|
},
|
||||||
|
|
||||||
|
// Twitter Card
|
||||||
|
{
|
||||||
|
'hid': 'twitter:title',
|
||||||
|
'property': 'twitter:title',
|
||||||
|
'content': this.title
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'hid': 'twitter:description',
|
||||||
|
'property': 'twitter:description',
|
||||||
|
'content': this.description
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'property': 'twitter:url',
|
||||||
|
'content': `${process.env.URL}${this.$nuxt.$route.path}`
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -1,14 +1,16 @@
|
||||||
<template>
|
<template>
|
||||||
<section class="container py-32">
|
<section class="container py-32">
|
||||||
<div class="flex flex-col items-center text-center">
|
<div class="flex flex-col items-center text-center space-y-8">
|
||||||
<h1 class="title text-sm md:text-2xl font-semibold">
|
<div class="hero-heading space-y-6">
|
||||||
Does {{ app.name }} work on Apple Silicon?
|
<h1 class="title text-sm md:text-2xl font-semibold">
|
||||||
</h1>
|
Does {{ app.name }} work on Apple Silicon?
|
||||||
<h2 class="subtitle text-2xl md:text-5xl font-bold py-6">
|
</h1>
|
||||||
{{ app.text }}
|
<h2 class="subtitle text-2xl md:text-5xl font-bold">
|
||||||
</h2>
|
{{ app.text }}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="subscribe space-y-6 sm:space-x-6 mb-4">
|
<div class="subscribe">
|
||||||
<AllUpdatesSubscribe
|
<AllUpdatesSubscribe
|
||||||
:app-name="app.name"
|
:app-name="app.name"
|
||||||
/>
|
/>
|
||||||
|
|
@ -24,6 +26,18 @@
|
||||||
>{{ (i === 0) ? 'View' : link.label }}</LinkButton>
|
>{{ (i === 0) ? 'View' : link.label }}</LinkButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="relatedVideos.length !== 0"
|
||||||
|
class="related-videos w-full"
|
||||||
|
>
|
||||||
|
<h2 class="subtitle text-xl md:text-2xl font-bold mb-3">
|
||||||
|
Related Videos
|
||||||
|
</h2>
|
||||||
|
<VideoRow
|
||||||
|
:videos="relatedVideos"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="report-links py-24 text-xs shadow-none">
|
<div class="report-links py-24 text-xs shadow-none">
|
||||||
<div v-if="app.lastUpdated">
|
<div v-if="app.lastUpdated">
|
||||||
<time
|
<time
|
||||||
|
|
@ -48,19 +62,43 @@
|
||||||
import parseGithubDate from '~/helpers/parse-github-date'
|
import parseGithubDate from '~/helpers/parse-github-date'
|
||||||
import LinkButton from '~/components/link-button.vue'
|
import LinkButton from '~/components/link-button.vue'
|
||||||
import AllUpdatesSubscribe from '~/components/all-updates-subscribe.vue'
|
import AllUpdatesSubscribe from '~/components/all-updates-subscribe.vue'
|
||||||
|
import VideoRow from '~/components/video/row.vue'
|
||||||
|
|
||||||
import appList from '~/static/app-list.json'
|
import appList from '~/static/app-list.json'
|
||||||
// import buildAppList from '~/helpers/build-app-list'
|
// import buildAppList from '~/helpers/build-app-list'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
LinkButton,
|
LinkButton,
|
||||||
AllUpdatesSubscribe
|
AllUpdatesSubscribe,
|
||||||
|
VideoRow
|
||||||
},
|
},
|
||||||
async asyncData ({ params: { slug } }) {
|
async asyncData ({ params: { slug } }) {
|
||||||
|
|
||||||
|
const { default: videoList } = await import('~/static/video-list.json')
|
||||||
|
const { videosRelatedToApp } = await import('~/helpers/related.js')
|
||||||
|
|
||||||
|
const app = appList.find(app => (app.slug === slug))
|
||||||
|
|
||||||
|
const relatedVideos = videosRelatedToApp(app)
|
||||||
|
|
||||||
|
// Find other videos that also feature this video's app
|
||||||
|
// for (const video of videoList) {
|
||||||
|
// if (!video.apps.includes(app.slug)) continue
|
||||||
|
|
||||||
|
// relatedVideos.push(video)
|
||||||
|
// }
|
||||||
|
|
||||||
return {
|
return {
|
||||||
slug,
|
slug,
|
||||||
app: appList.find(app => (app.slug === slug))
|
app,
|
||||||
|
relatedVideos: relatedVideos.map(video => {
|
||||||
|
// console.log('video', video)
|
||||||
|
return {
|
||||||
|
...video,
|
||||||
|
endpoint: `${slug}/benchmarks#${video.id}`
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
252
pages/benchmarks.vue
Normal file
252
pages/benchmarks.vue
Normal file
|
|
@ -0,0 +1,252 @@
|
||||||
|
<template>
|
||||||
|
<section class="container relative md:static overflow-hidden md:overflow-visible pb-16">
|
||||||
|
<div class="flex flex-col items-center text-center space-y-12">
|
||||||
|
<BgPlayer
|
||||||
|
:video="video"
|
||||||
|
class="absolute overflow-hidden w-2x-screen md:w-full pointer-events-none"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="page-heading flex justify-start w-full">
|
||||||
|
<h1 class="title text-2xl leading-tight mt-12 mb-6">
|
||||||
|
Benchmarks
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="line-separator border-white border-t-2 mb-12" />
|
||||||
|
|
||||||
|
<a
|
||||||
|
:href="video.endpoint"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="relative flex flex-col w-full justify-center items-center space-y-8 py-16 md:pt-0 md:pb-12 md:px-10"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="play-circle w-16 h-16 bg-white-2 bg-blur flex justify-center items-center outline-0 rounded-full ease"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 18 18"
|
||||||
|
style="width:24px;height:24px;margin-left:3px"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M15.562 8.1L3.87.225c-.818-.562-1.87 0-1.87.9v15.75c0 .9 1.052 1.462 1.87.9L15.563 9.9c.584-.45.584-1.35 0-1.8z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2 class="title text-lg md:text-2xl font-semibold">
|
||||||
|
{{ video.name }}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="features-apps w-full"
|
||||||
|
>
|
||||||
|
<hr class="w-full" >
|
||||||
|
<div class="featured-apps overflow-x-auto overflow-y-visible whitespace-no-wrap py-2 space-x-2">
|
||||||
|
<LinkButton
|
||||||
|
v-for="app in featuredApps"
|
||||||
|
:key="app.slug"
|
||||||
|
:href="app.endpoint"
|
||||||
|
:class="[
|
||||||
|
'inline-block text-xs rounded-lg py-1 px-2',
|
||||||
|
]"
|
||||||
|
:class-groups="{
|
||||||
|
shadow: 'neumorphic-shadow-inner'
|
||||||
|
}"
|
||||||
|
>{{ app.name }}</LinkButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-for="(row, key) in videoRows"
|
||||||
|
:key="key"
|
||||||
|
:class="`${key}-videos w-full max-w-4xl`"
|
||||||
|
>
|
||||||
|
<h2 class="subtitle text-xl md:text-2xl font-bold mb-3">
|
||||||
|
{{ row.heading }}
|
||||||
|
</h2>
|
||||||
|
<!-- <pre class="text-left">{{ benchmarkVideos }}</pre> -->
|
||||||
|
<VideoRow
|
||||||
|
:videos="row.videos"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import { getVideoEndpoint, getAppEndpoint } from '~/helpers/app-derived.js'
|
||||||
|
|
||||||
|
import LinkButton from '~/components/link-button.vue'
|
||||||
|
import EmailSubscribe from '~/components/email-subscribe.vue'
|
||||||
|
import VideoRow from '~/components/video/row.vue'
|
||||||
|
import BgPlayer from '~/components/video/bg-player.vue'
|
||||||
|
import ChannelCredit from '~/components/video/channel-credit.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
LinkButton,
|
||||||
|
EmailSubscribe,
|
||||||
|
VideoRow,
|
||||||
|
BgPlayer,
|
||||||
|
ChannelCredit
|
||||||
|
},
|
||||||
|
async asyncData ({ params: { slug } }) {
|
||||||
|
|
||||||
|
const { appsRelatedToVideo } = await import('~/helpers/related.js')
|
||||||
|
const { default: videoList } = await import('~/static/video-list.json')
|
||||||
|
|
||||||
|
// Get featured apps
|
||||||
|
const featuredAppsSet = new Set()
|
||||||
|
|
||||||
|
videoList.slice(0, 24).forEach( video => {
|
||||||
|
appsRelatedToVideo(video).forEach( app => {
|
||||||
|
featuredAppsSet.add(app)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
video: videoList[0],
|
||||||
|
featuredApps: Array.from(featuredAppsSet).map( app => {
|
||||||
|
return {
|
||||||
|
...app,
|
||||||
|
endpoint: getAppEndpoint(app) + '/benchmarks'
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
allVideos: videoList.map( video => {
|
||||||
|
return {
|
||||||
|
...video,
|
||||||
|
endpoint: getVideoEndpoint(video)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
videoRows: {
|
||||||
|
'video-benchmarks': {
|
||||||
|
heading: 'Video Editing Benchmarks',
|
||||||
|
matchesCondition: video => {
|
||||||
|
return video.tags.includes('benchmark') && video.tags.includes('video-and-motion-tools')
|
||||||
|
},
|
||||||
|
videos: []
|
||||||
|
},
|
||||||
|
'music-and-audio-tools': {
|
||||||
|
heading: 'Music and DAW Performance',
|
||||||
|
matchesCondition: video => {
|
||||||
|
return video.tags.includes('music-and-audio-tools')
|
||||||
|
},
|
||||||
|
videos: []
|
||||||
|
},
|
||||||
|
// 'science-and-research-software': {
|
||||||
|
// heading: 'Science and Research',
|
||||||
|
// matchesCondition: video => {
|
||||||
|
// return video.tags.includes('science-and-research-software')
|
||||||
|
// },
|
||||||
|
// videos: []
|
||||||
|
// },
|
||||||
|
'photo-and-graphic-tools': {
|
||||||
|
heading: 'Photography and Design Compatibility',
|
||||||
|
matchesCondition: video => {
|
||||||
|
return video.tags.includes('photo-and-graphic-tools')
|
||||||
|
},
|
||||||
|
videos: []
|
||||||
|
},
|
||||||
|
'games': {
|
||||||
|
heading: 'Gaming Benchmarks',
|
||||||
|
matchesCondition: video => {
|
||||||
|
return video.tags.includes('benchmark') && video.tags.includes('games')
|
||||||
|
},
|
||||||
|
videos: []
|
||||||
|
},
|
||||||
|
'benchmarks': {
|
||||||
|
heading: 'Other Benchmark Videos',
|
||||||
|
matchesCondition: video => video.tags.includes('benchmark'),
|
||||||
|
videos: []
|
||||||
|
},
|
||||||
|
'performance': {
|
||||||
|
heading: 'Performance Videos',
|
||||||
|
matchesCondition: video => video.tags.includes('performance'),
|
||||||
|
videos: []
|
||||||
|
},
|
||||||
|
|
||||||
|
'other': {
|
||||||
|
heading: 'More Videos',
|
||||||
|
// Always true
|
||||||
|
matchesCondition: () => true,
|
||||||
|
videos: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
title () {
|
||||||
|
return `Benchmarks for Apple M1 and Apple Silicon - Does It ARM`
|
||||||
|
},
|
||||||
|
description () {
|
||||||
|
// const featuredAppsString = this.featuredApps.slice(0, 5).map(app => app.name).join(', ')
|
||||||
|
|
||||||
|
return `Apple Silicon benchmark, performance, and compatibility videos`
|
||||||
|
},
|
||||||
|
activeVideoId () {
|
||||||
|
return this.video.id
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
|
||||||
|
// Move videos to relevant categories
|
||||||
|
this.allVideos.forEach((video, index) => {
|
||||||
|
// console.log('video.name', video.name)
|
||||||
|
// console.log('video.tags', video.tags)
|
||||||
|
|
||||||
|
// Look through row conditions to see if video matches
|
||||||
|
for (const row in this.videoRows) {
|
||||||
|
if( this.videoRows[row].matchesCondition(video) ) {
|
||||||
|
|
||||||
|
// Add the matching video
|
||||||
|
this.videoRows[row].videos.push(video)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('lengths', Object.values(this.videoRows).map(row => [row.heading, row.videos.length]))
|
||||||
|
|
||||||
|
},
|
||||||
|
head() {
|
||||||
|
return {
|
||||||
|
title: this.title,
|
||||||
|
meta: [
|
||||||
|
// hid is used as unique identifier. Do not use `vmid` for it as it will not work
|
||||||
|
{
|
||||||
|
'hid': 'description',
|
||||||
|
'name': 'description',
|
||||||
|
'content': this.description
|
||||||
|
},
|
||||||
|
|
||||||
|
// Twitter Card
|
||||||
|
{
|
||||||
|
'hid': 'twitter:title',
|
||||||
|
'property': 'twitter:title',
|
||||||
|
'content': this.title
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'hid': 'twitter:description',
|
||||||
|
'property': 'twitter:description',
|
||||||
|
'content': this.description
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'property': 'twitter:url',
|
||||||
|
'content': `${process.env.URL}${this.$nuxt.$route.path}`
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
220
pages/game/_slug/benchmarks.vue
Normal file
220
pages/game/_slug/benchmarks.vue
Normal file
|
|
@ -0,0 +1,220 @@
|
||||||
|
<template>
|
||||||
|
<section class="container pb-16">
|
||||||
|
<div class="flex flex-col items-center text-center space-y-8">
|
||||||
|
<template
|
||||||
|
v-if="video"
|
||||||
|
>
|
||||||
|
<VideoPlayer
|
||||||
|
:video="video"
|
||||||
|
class="pt-16"
|
||||||
|
/>
|
||||||
|
<ChannelCredit
|
||||||
|
:video="video"
|
||||||
|
class="flex w-full justify-start md:px-10"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div
|
||||||
|
:style="{
|
||||||
|
'left': '50%',
|
||||||
|
'right': '50%',
|
||||||
|
'margin-left': '-50vw',
|
||||||
|
'margin-right': '-50vw'
|
||||||
|
}"
|
||||||
|
class="video-canvas w-screen flex justify-center bg-black pt-16"
|
||||||
|
>
|
||||||
|
<div class="ratio-wrapper w-full max-w-4xl">
|
||||||
|
<div class="relative overflow-hidden w-full pb-16/9">
|
||||||
|
<div class="absolute h-full w-full flex justify-center items-center">
|
||||||
|
<div class="message text-4xl md:text-6xl font-hairline leading-tight text-center">No videos yet</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-for="(row, key) in videoRows"
|
||||||
|
:key="key"
|
||||||
|
class="w-full max-w-4xl"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="row.videos.length !== 0"
|
||||||
|
:class="`${key}-videos w-full`"
|
||||||
|
>
|
||||||
|
<h2 class="subtitle text-xl md:text-2xl font-bold mb-3">
|
||||||
|
{{ row.heading }}
|
||||||
|
</h2>
|
||||||
|
<!-- <pre class="text-left">{{ benchmarkVideos }}</pre> -->
|
||||||
|
<VideoRow
|
||||||
|
:videos="row.videos"
|
||||||
|
:active-video-id="activeVideoId"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import LinkButton from '~/components/link-button.vue'
|
||||||
|
import EmailSubscribe from '~/components/email-subscribe.vue'
|
||||||
|
import VideoRow from '~/components/video/row.vue'
|
||||||
|
import VideoPlayer from '~/components/video/player.vue'
|
||||||
|
import ChannelCredit from '~/components/video/channel-credit.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
LinkButton,
|
||||||
|
EmailSubscribe,
|
||||||
|
VideoRow,
|
||||||
|
VideoPlayer,
|
||||||
|
ChannelCredit
|
||||||
|
},
|
||||||
|
async asyncData ({ params: { slug } }) {
|
||||||
|
|
||||||
|
const { allVideoAppsList } = await import('~/helpers/get-list.js')
|
||||||
|
// const { default: videoList } = await import('~/static/video-list.json')
|
||||||
|
|
||||||
|
const { videosRelatedToApp } = await import('~/helpers/related.js')
|
||||||
|
|
||||||
|
const app = allVideoAppsList.find(app => (app.slug === slug))
|
||||||
|
|
||||||
|
// const submitVideoCard = {
|
||||||
|
// endpoint: `https://docs.google.com/forms/d/e/1FAIpQLSeEVGM9vE7VcfLMy6fJkfU70X2VZ60rHDyhDQLtnAN4nso0WA/viewform?usp=pp_url&entry.1018125313=${app.name}`
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const featuredApps = []
|
||||||
|
|
||||||
|
const relatedVideos = videosRelatedToApp( app ).map(video => {
|
||||||
|
// console.log('video', video)
|
||||||
|
return {
|
||||||
|
...video,
|
||||||
|
// endpoint: `#${video.id}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
app,
|
||||||
|
allVideos: relatedVideos,
|
||||||
|
// submitVideoCard
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
activeVideoIndex: 0,
|
||||||
|
videoRows: {
|
||||||
|
'benchmarks': {
|
||||||
|
heading: 'Benchmark Videos',
|
||||||
|
matchesCondition: video => video.tags.includes('benchmark'),
|
||||||
|
videos: []
|
||||||
|
},
|
||||||
|
'performance': {
|
||||||
|
heading: 'Performance Videos',
|
||||||
|
matchesCondition: video => video.tags.includes('performance'),
|
||||||
|
videos: []
|
||||||
|
},
|
||||||
|
|
||||||
|
'other': {
|
||||||
|
heading: 'More Videos',
|
||||||
|
// Always true
|
||||||
|
matchesCondition: () => true,
|
||||||
|
videos: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
video () {
|
||||||
|
return this.allVideos[this.activeVideoIndex]
|
||||||
|
},
|
||||||
|
title () {
|
||||||
|
return `${this.app.name} Benchmarks for Apple Silicon - Does It ARM`
|
||||||
|
},
|
||||||
|
description () {
|
||||||
|
return `Apple Silicon gaming benchmark, performance, and support videos for ${this.app.name}`
|
||||||
|
},
|
||||||
|
activeVideoId () {
|
||||||
|
return (this.video === Object(this.video)) ? this.video.id : null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
|
||||||
|
// Move videos to relevant categories
|
||||||
|
this.allVideos.forEach((video, index) => {
|
||||||
|
// console.log('video.name', video.name)
|
||||||
|
// console.log('video.tags', video.tags)
|
||||||
|
|
||||||
|
// Look through row conditions to see if video matches
|
||||||
|
for (const row in this.videoRows) {
|
||||||
|
if( this.videoRows[row].matchesCondition(video) ) {
|
||||||
|
|
||||||
|
// Add the matching video
|
||||||
|
this.videoRows[row].videos.push(video)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('lengths', Object.values(this.videoRows).map(row => [row.heading, row.videos.length]))
|
||||||
|
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
window.onhashchange = this.loadVideoFromHash
|
||||||
|
|
||||||
|
if (location.hash.length !== 0) this.loadVideoFromHash()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
loadVideoFromHash () {
|
||||||
|
// console.log('location.hash', location.hash)
|
||||||
|
|
||||||
|
// Separate the video id from our window hash
|
||||||
|
const hashId = location.hash.split('#')[1]
|
||||||
|
|
||||||
|
// Find the index of the video with the matching hash
|
||||||
|
const newVideoIndex = this.allVideos.findIndex(video => {
|
||||||
|
return video.id === hashId
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('newVideoIndex', newVideoIndex)
|
||||||
|
|
||||||
|
// Load in the index to load out video
|
||||||
|
this.activeVideoIndex = newVideoIndex
|
||||||
|
|
||||||
|
window.scroll({ top: 0, behavior: 'smooth' })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
head() {
|
||||||
|
return {
|
||||||
|
title: this.title,
|
||||||
|
meta: [
|
||||||
|
// hid is used as unique identifier. Do not use `vmid` for it as it will not work
|
||||||
|
{
|
||||||
|
'hid': 'description',
|
||||||
|
'name': 'description',
|
||||||
|
'content': this.description
|
||||||
|
},
|
||||||
|
|
||||||
|
// Twitter Card
|
||||||
|
{
|
||||||
|
'hid': 'twitter:title',
|
||||||
|
'property': 'twitter:title',
|
||||||
|
'content': this.title
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'hid': 'twitter:description',
|
||||||
|
'property': 'twitter:description',
|
||||||
|
'content': this.description
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'property': 'twitter:url',
|
||||||
|
'content': `${process.env.URL}${this.$nuxt.$route.path}`
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -34,6 +34,27 @@ import ThomasCredit from '~/components/thomas-credit.vue'
|
||||||
import gameList from '~/static/game-list.json'
|
import gameList from '~/static/game-list.json'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
async asyncData () {
|
||||||
|
const { sortedAppList, allList, allVideoAppsList, makeAppSearchLinks } = await import('~/helpers/get-list.js')
|
||||||
|
const { default: gameList } = await import('~/static/game-list.json')
|
||||||
|
|
||||||
|
return {
|
||||||
|
// Map game list
|
||||||
|
gameList: gameList.map( app => {
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: app.name,
|
||||||
|
status: app.status,
|
||||||
|
slug: app.slug,
|
||||||
|
// endpoint: app.endpoint,
|
||||||
|
text: app.text,
|
||||||
|
lastUpdated: app.lastUpdated,
|
||||||
|
category: app.category,
|
||||||
|
searchLinks: makeAppSearchLinks(app)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
components: {
|
components: {
|
||||||
Search,
|
Search,
|
||||||
LinkButton,
|
LinkButton,
|
||||||
|
|
@ -59,9 +80,9 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
gameList() {
|
// gameList() {
|
||||||
return gameList
|
// return gameList
|
||||||
}
|
// }
|
||||||
},
|
},
|
||||||
head() {
|
head() {
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
<Search
|
<Search
|
||||||
:app-list="allList"
|
:app-list="allList"
|
||||||
:quick-buttons="quickButtons"
|
:quick-buttons="quickButtons"
|
||||||
:initial-limit="200"
|
:initial-limit="100"
|
||||||
@update:query="onQueryUpdate"
|
@update:query="onQueryUpdate"
|
||||||
>
|
>
|
||||||
<template v-slot:before-search>
|
<template v-slot:before-search>
|
||||||
|
|
@ -71,13 +71,33 @@ export default {
|
||||||
// const { default: appList } = await import('~/static/app-list.json')
|
// const { default: appList } = await import('~/static/app-list.json')
|
||||||
// const { default: gamelist } = await import('~/static/game-list.json')
|
// const { default: gamelist } = await import('~/static/game-list.json')
|
||||||
|
|
||||||
const { sortedAppList, allList } = await import('~/helpers/get-list.js')
|
const { sortedAppList, allList, allVideoAppsList, makeAppSearchLinks } = await import('~/helpers/get-list.js')
|
||||||
|
|
||||||
|
const allAppSearchLinks = {}
|
||||||
|
|
||||||
|
// console.log('allVideoAppsList', allVideoAppsList)
|
||||||
|
|
||||||
|
allVideoAppsList.forEach( app => {
|
||||||
|
// Make the search links
|
||||||
|
const searchLinks = makeAppSearchLinks(app)
|
||||||
|
|
||||||
|
// If there are more than zero
|
||||||
|
// add them to our list
|
||||||
|
if (searchLinks.length > 0) {
|
||||||
|
allAppSearchLinks[app.slug] = searchLinks
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// Filter app list to leave out data not needed for search
|
// Filter app list to leave out data not needed for search
|
||||||
initialAppList: sortedAppList.map( app => {
|
initialAppList: sortedAppList.map( app => {
|
||||||
|
|
||||||
|
let searchLinks = []
|
||||||
|
|
||||||
|
if (typeof allAppSearchLinks[app.slug] !== 'undefined') {
|
||||||
|
searchLinks = allAppSearchLinks[app.slug]
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: app.name,
|
name: app.name,
|
||||||
status: app.status,
|
status: app.status,
|
||||||
|
|
@ -86,8 +106,10 @@ export default {
|
||||||
text: app.text,
|
text: app.text,
|
||||||
lastUpdated: app.lastUpdated,
|
lastUpdated: app.lastUpdated,
|
||||||
category: app.category,
|
category: app.category,
|
||||||
|
searchLinks
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
allAppSearchLinks,
|
||||||
customSummaryNumbers: getListSummaryNumbers(allList)
|
customSummaryNumbers: getListSummaryNumbers(allList)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -163,6 +185,8 @@ export default {
|
||||||
// then stop
|
// then stop
|
||||||
if (this.fetchedAppList.length !== 0 || this.query.trim().length === 0) return
|
if (this.fetchedAppList.length !== 0 || this.query.trim().length === 0) return
|
||||||
|
|
||||||
|
// console.log('this.allAppSearchLinks', this.allAppSearchLinks)
|
||||||
|
|
||||||
const fetchedListUrls = [
|
const fetchedListUrls = [
|
||||||
'/game-list.json',
|
'/game-list.json',
|
||||||
'/homebrew-list.json'
|
'/homebrew-list.json'
|
||||||
|
|
@ -179,7 +203,19 @@ export default {
|
||||||
|
|
||||||
// console.log('fetchedLists', fetchedLists)
|
// console.log('fetchedLists', fetchedLists)
|
||||||
|
|
||||||
this.fetchedAppList = fetchedLists.flat(1)
|
this.fetchedAppList = fetchedLists.flat(1).map( app => {
|
||||||
|
|
||||||
|
let searchLinks = []
|
||||||
|
|
||||||
|
if (typeof this.allAppSearchLinks[app.slug] !== 'undefined') {
|
||||||
|
searchLinks = this.allAppSearchLinks[app.slug]
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...app,
|
||||||
|
searchLinks
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,26 +55,31 @@
|
||||||
import Search from '~/components/search.vue'
|
import Search from '~/components/search.vue'
|
||||||
import LinkButton from '~/components/link-button.vue'
|
import LinkButton from '~/components/link-button.vue'
|
||||||
|
|
||||||
import { byTimeThenNull } from '~/helpers/sort-list.js'
|
|
||||||
|
|
||||||
import { categories, getAppCategory } from '~/helpers/categories.js'
|
import { categories, getAppCategory } from '~/helpers/categories.js'
|
||||||
|
|
||||||
import appList from '~/static/app-list.json'
|
|
||||||
import gamelist from '~/static/game-list.json'
|
|
||||||
import homebrewList from '~/static/homebrew-list.json'
|
|
||||||
|
|
||||||
const allList = [
|
|
||||||
...appList.sort(byTimeThenNull),
|
|
||||||
...homebrewList,
|
|
||||||
...gamelist,
|
|
||||||
]
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
async asyncData ({ params: { slug } }) {
|
async asyncData ({ params: { slug } }) {
|
||||||
// Maybe I could import() here to reduce client script size
|
const { sortedAppList, allList, allVideoAppsList, makeAppSearchLinks } = await import('~/helpers/get-list.js')
|
||||||
|
const { default: gameList } = await import('~/static/game-list.json')
|
||||||
|
|
||||||
|
const filteredList = allList.filter(app => {
|
||||||
|
return app.category.slug === slug
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
slug,
|
slug,
|
||||||
// app: appList.find(app => (app.slug === slug))
|
categoryAppList: filteredList.map( app => {
|
||||||
|
return {
|
||||||
|
name: app.name,
|
||||||
|
status: app.status,
|
||||||
|
slug: app.slug,
|
||||||
|
// endpoint: app.endpoint,
|
||||||
|
text: app.text,
|
||||||
|
lastUpdated: app.lastUpdated,
|
||||||
|
category: app.category,
|
||||||
|
searchLinks: makeAppSearchLinks(app)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
|
@ -104,20 +109,6 @@ export default {
|
||||||
category () {
|
category () {
|
||||||
return categories[this.slug]
|
return categories[this.slug]
|
||||||
},
|
},
|
||||||
categoryAppList () {
|
|
||||||
|
|
||||||
const filteredList = allList.filter(app => {
|
|
||||||
return app.category.slug === this.slug
|
|
||||||
})
|
|
||||||
|
|
||||||
// const sortedList = list.sort(byTimeThenNull)
|
|
||||||
|
|
||||||
// if (this.category.slug === 'homebrew') {
|
|
||||||
// return filteredList.slice(0, 300)
|
|
||||||
// }
|
|
||||||
|
|
||||||
return filteredList
|
|
||||||
},
|
|
||||||
supportedAppList () {
|
supportedAppList () {
|
||||||
return this.categoryAppList.filter(app => {
|
return this.categoryAppList.filter(app => {
|
||||||
return app.status.includes('yes')
|
return app.status.includes('yes')
|
||||||
|
|
|
||||||
153
pages/tv/_slug.vue
Normal file
153
pages/tv/_slug.vue
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
<template>
|
||||||
|
<section class="container pb-16">
|
||||||
|
<div class="flex flex-col items-center text-center space-y-6">
|
||||||
|
<VideoPlayer
|
||||||
|
:video="video"
|
||||||
|
class="pt-16"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="md:flex w-full justify-between space-y-4 md:space-y-0 md:px-10"
|
||||||
|
>
|
||||||
|
<h1 class="title text-lg md:text-2xl font-semibold">
|
||||||
|
{{ video.name }}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<ChannelCredit
|
||||||
|
:video="video"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="w-full" >
|
||||||
|
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="featuredApps.length !== 0"
|
||||||
|
class="related-apps w-full"
|
||||||
|
>
|
||||||
|
<h2 class="subtitle text-xl md:text-2xl font-bold mb-3">
|
||||||
|
Related Apps
|
||||||
|
</h2>
|
||||||
|
<div class="featured-apps overflow-x-auto overflow-y-visible whitespace-no-wrap py-2 space-x-2">
|
||||||
|
<LinkButton
|
||||||
|
v-for="app in featuredApps"
|
||||||
|
:key="app.slug"
|
||||||
|
:href="getAppEndpoint(app)"
|
||||||
|
:class="[
|
||||||
|
'inline-block text-xs rounded-lg py-1 px-2',
|
||||||
|
]"
|
||||||
|
:class-groups="{
|
||||||
|
shadow: 'neumorphic-shadow-inner'
|
||||||
|
}"
|
||||||
|
>{{ app.name }}</LinkButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="related-videos w-full">
|
||||||
|
<h2 class="subtitle text-xl md:text-2xl font-bold mb-3">
|
||||||
|
Related Videos
|
||||||
|
</h2>
|
||||||
|
<VideoRow
|
||||||
|
:videos="relatedVideos"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- video: {{ video }} -->
|
||||||
|
|
||||||
|
<!-- <div class="links space-y-6 sm:space-x-6 mb-8">
|
||||||
|
<LinkButton
|
||||||
|
v-for="(link, i) in app.relatedLinks"
|
||||||
|
:key="i"
|
||||||
|
:href="link.href"
|
||||||
|
target="_blank"
|
||||||
|
class=""
|
||||||
|
>{{ (i === 0) ? 'View' : link.label }}</LinkButton>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import { getAppEndpoint } from '~/helpers/app-derived.js'
|
||||||
|
|
||||||
|
import LinkButton from '~/components/link-button.vue'
|
||||||
|
import EmailSubscribe from '~/components/email-subscribe.vue'
|
||||||
|
import VideoRow from '~/components/video/row.vue'
|
||||||
|
import VideoPlayer from '~/components/video/player.vue'
|
||||||
|
import ChannelCredit from '~/components/video/channel-credit.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
LinkButton,
|
||||||
|
EmailSubscribe,
|
||||||
|
VideoRow,
|
||||||
|
VideoPlayer,
|
||||||
|
ChannelCredit
|
||||||
|
},
|
||||||
|
async asyncData ({ params: { slug } }) {
|
||||||
|
|
||||||
|
const { appsRelatedToVideo, videosRelatedToVideo } = await import('~/helpers/related.js')
|
||||||
|
const { default: videoList } = await import('~/static/video-list.json')
|
||||||
|
|
||||||
|
// Find the video for our current page
|
||||||
|
const video = videoList.find(video => (video.slug === slug))
|
||||||
|
|
||||||
|
// Get featured apps
|
||||||
|
const featuredApps = appsRelatedToVideo(video)
|
||||||
|
|
||||||
|
// Get related videos
|
||||||
|
const relatedVideos = videosRelatedToVideo(video)
|
||||||
|
|
||||||
|
return {
|
||||||
|
video,
|
||||||
|
featuredApps,
|
||||||
|
// If no related video found just get the 12 newest ones
|
||||||
|
relatedVideos: (relatedVideos.length !== 0) ? relatedVideos : videoList.slice(0, 12)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
title () {
|
||||||
|
return `${this.video.name} - Does It ARM`
|
||||||
|
},
|
||||||
|
description () {
|
||||||
|
const featuredAppsString = this.featuredApps.slice(0, 5).map(app => app.name).join(', ')
|
||||||
|
|
||||||
|
return `Apple Silicon performance and support videos for ${featuredAppsString}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getAppEndpoint
|
||||||
|
},
|
||||||
|
head() {
|
||||||
|
return {
|
||||||
|
title: this.title,
|
||||||
|
meta: [
|
||||||
|
// hid is used as unique identifier. Do not use `vmid` for it as it will not work
|
||||||
|
{
|
||||||
|
'hid': 'description',
|
||||||
|
'name': 'description',
|
||||||
|
'content': this.description
|
||||||
|
},
|
||||||
|
|
||||||
|
// Twitter Card
|
||||||
|
{
|
||||||
|
'hid': 'twitter:title',
|
||||||
|
'property': 'twitter:title',
|
||||||
|
'content': this.title
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'hid': 'twitter:description',
|
||||||
|
'property': 'twitter:description',
|
||||||
|
'content': this.description
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'property': 'twitter:url',
|
||||||
|
'content': `${process.env.URL}${this.$nuxt.$route.path}`
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -75,7 +75,13 @@ module.exports = {
|
||||||
'1/2-screen': '50vh',
|
'1/2-screen': '50vh',
|
||||||
'full-screen': '100vh'
|
'full-screen': '100vh'
|
||||||
},
|
},
|
||||||
|
width: {
|
||||||
|
'1/2-screen': '50vw',
|
||||||
|
'full-screen': '100vw',
|
||||||
|
'2x-screen': '200vw'
|
||||||
|
},
|
||||||
minHeight: {
|
minHeight: {
|
||||||
|
'1/2-screen': '50vh',
|
||||||
'3/4-screen': '75vh',
|
'3/4-screen': '75vh',
|
||||||
},
|
},
|
||||||
spacing: {
|
spacing: {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue