Merge branch 'feat/astro'

This commit is contained in:
Sam Carlton 2022-06-13 09:48:07 -05:00
commit 60f7cfa451
109 changed files with 17945 additions and 3583 deletions

22
.gitignore vendored
View file

@ -68,9 +68,6 @@ typings/
# nuxt.js build output
.nuxt
# Nuxt generate
dist
# vuepress build output
.vuepress/dist
@ -80,12 +77,23 @@ dist
# IDE
.idea
# Stork Executable
/stork-executable
# Local Netlify folder
.netlify
# Build Output
dist
/static/sitemap*
/static/app-list.json
/static/**/*.json
/static/**/*.toml
/static/**/*.st
/commits-data.json
/static/tailwind.css
# Other
/static/app-list.json
/README-temp.md
/static/**/*.json
/commits-data.json
.DS_Store
/static/tailwind.css
/.vscode/snipsnap.code-snippets

2
.nvmrc
View file

@ -1 +1 @@
v16.13
v18

4
.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,4 @@
{
"recommendations": ["astro-build.astro-vscode"],
"unwantedRecommendations": []
}

11
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"command": "./node_modules/.bin/astro dev",
"name": "Development server",
"request": "launch",
"type": "node-terminal"
}
]
}

View file

@ -90,9 +90,9 @@ html {
* @import "utilities/skew-transforms";
*/
.container {
/* .container {
max-width: 1040px;
}
} */
.ease {
transition-property: all;
@ -153,19 +153,79 @@ html {
.shimmer {
animation: placeHolderShimmer 1s infinite;
animation-timing-function: linear;
background: #f6f7f8;
background: linear-gradient(to right, rgba(0, 0, 0, 0.07) 8%, rgba(0, 0, 0, 0.45) 18%, rgba(0, 0, 0, 0.07) 33%);
background-color: rgba( 0, 0, 0, 0.1 );
background-image: linear-gradient( 90deg, rgba(0, 0, 0, 0.07) 8%, rgba(0, 0, 0, 0.45) 18%, rgba(0, 0, 0, 0.07) 33%);
background-size: 200% 100px;
background-position: 0 0, 100% 0;
background-attachment: fixed;
}
/* Original - https://codepen.io/ThatGuySam/pen/zYPPYwg */
/* Animation delay so our shimmer looks staggered */
.shimmer *:nth-child(3n-2),
*:nth-child(3n-2) > .shimmer {
animation-delay: 0.2s;
}
.shimmer *:nth-child(3n-1),
*:nth-child(3n-1) > .shimmer {
animation-delay: 0.1s;
}
.shimmer,
.shimmer * {
/* Hide all text */
color: transparent;
}
.shimmer p,
.shimmer img,
.shimmer span,
.shimmer time,
.shimmer h1, .shimmer h2, .shimmer h3, .shimmer h4, .shimmer h5, .shimmer h6 {
animation: placeHolderShimmer 1s infinite;
animation-timing-function: linear;
/* Shimmer gets inserted as an animated background so it can shape to most elements */
/* Base shimmer color */
background-color: rgba( 0, 0, 0, 0.1 );
/* First and last color should be the same so that animation restart is seamless */
background-image: linear-gradient(
90deg,
rgba(0, 0, 0, 0.07) 8%,
rgba(0, 0, 0, 0.13) 18%,
rgba(0, 0, 0, 0.07) 33%
);
background-size: 200% 100px;
background-attachment: fixed;
border: none;
/* Hide all text */
color: transparent;
/* Hide img src */
object-position: -99999px 99999px;
}
/* Inline Shimmers so we get separated shimmer lines on text */
.shimmer p,
.shimmer span,
.shimmer time {
display: inline;
}
@keyframes placeHolderShimmer {
0% {
background-position: 100% 0;
}
100% {
background-position: -100% 0;
}
0% {
background-position: 100% 0
}
100% {
background-position: -100% 0
}
}

61
astro.config.mjs Normal file
View file

@ -0,0 +1,61 @@
import { defineConfig } from 'astro/config'
import vue from '@astrojs/vue'
import tailwind from '@astrojs/tailwind'
// Astro Netlify Reference
// https://github.com/withastro/astro/tree/main/packages/integrations/netlify
import netlify from '@astrojs/netlify/functions'
// import sitemap from '@astrojs/sitemap'
import partytown from '@astrojs/partytown'
// import { viteCommonjs } from '@originjs/vite-plugin-commonjs'
import { makeViteDefinitions } from './helpers/public-runtime-config.mjs'
console.log( 'Running Astro Config File' )
// https://astro.build/config
export default defineConfig({
publicDir: './static',
site: 'https://doesitarm.com',
integrations: [
netlify({
dist: new URL('./dist/', import.meta.url)
}),
// Astro Vue Reference
// https://github.com/withastro/astro/tree/main/packages/integrations/vue
vue(),
tailwind(),
// Sitemap Reference
// https://github.com/withastro/astro/blob/main/packages/integrations/sitemap/src/index.ts
// https://github.com/withastro/astro/tree/main/packages/integrations/sitemap#configuration
// sitemap({
// customPages: [
// '/relative-url',
// 'https://doesitarm.com/absolute-url',
// ]
// })
partytown({
// Add dataLayer.push as a forwarding-event.
// https://github.com/withastro/astro/tree/main/packages/integrations/partytown#configforward
config: { forward: [ 'dataLayer.push' ] },
}),
],
// Vite options
// https://docs.astro.build/en/reference/configuration-reference/#vite
vite: {
// Vite: https://vitejs.dev/config/#define
// esbuild: https://esbuild.github.io/api/#define
define: {
...makeViteDefinitions()
},
// plugins: [
// viteCommonjs()
// ],
build: {
commonjsOptions: {
transformMixedEsModules: true
}
}
}
})

19
ava.config.js Normal file
View file

@ -0,0 +1,19 @@
export default () => {
return {
require: [
'dotenv/config',
'esm',
'tsconfig-paths/register'
],
// https://github.com/avajs/ava/blob/main/docs/recipes/watch-mode.md
ignoredByWatcher: [
'!**/*.{js,vue}',
'./build',
'./dist',
'./.output',
],
// tap: true,
// verbose: true,
color: true
}
}

View file

@ -1,38 +1,90 @@
import { dirname } from 'path'
import { dirname, basename } from 'path'
import fs from 'fs-extra'
import dotenv from 'dotenv'
import semver from 'semver'
import { PromisePool } from '@supercharge/promise-pool'
import memoize from 'fast-memoize'
import has from 'just-has'
import buildAppList from './helpers/build-app-list.js'
import buildGamesList from './helpers/build-game-list.js'
import buildHomebrewList from './helpers/build-homebrew-list.js'
import buildVideoList from './helpers/build-video-list.js'
import buildDeviceList from './helpers/build-device-list.js'
import { saveYouTubeVideos } from '~/helpers/api/youtube/build.js'
import buildAppList from '~/helpers/build-app-list.js'
import buildGamesList from '~/helpers/build-game-list.js'
import buildHomebrewList from '~/helpers/build-homebrew-list.js'
import buildVideoList from '~/helpers/build-video-list.js'
import buildDeviceList from '~/helpers/build-device-list.js'
import {
saveSitemap,
getUrlsForAstroDefinedPages
} from '~/helpers/api/sitemap/build.js'
import { deviceSupportsApp } from '~/helpers/devices.js'
import getListSummaryNumbers from '~/helpers/get-list-summary-numbers.js'
import { logArraysDifference } from '~/helpers/array.js'
import { videosRelatedToApp } from './helpers/related.js'
import { buildVideoPayload, buildAppBenchmarkPayload } from './helpers/build-payload.js'
import {
getRelatedVideos
} from '~/helpers/related.js'
import { buildVideoPayload, buildAppBenchmarkPayload } from '~/helpers/build-payload.js'
import { categories, getAppCategory } from './helpers/categories.js'
import {
categories,
getCategoryKindName
} from '~/helpers/categories.js'
import {
getAppType,
getAppEndpoint,
getVideoEndpoint,
isVideo
} from './helpers/app-derived.js'
import { makeSearchableList } from './helpers/searchable-list.js'
} from '~/helpers/app-derived.js'
import { makeSearchableList } from '~/helpers/searchable-list.js'
import {
writeStorkToml
} from '~/helpers/stork/toml.js'
import {
KindListMemoized as KindList
} from '~/helpers/api/kind.js'
import {
apiDirectory
} from '~/helpers/api/config.js'
import {
cliOptions
} from '~/helpers/cli-options.js'
// Setup dotenv
dotenv.config()
const commandArguments = process.argv
const cliOptions = {
withApi: commandArguments.includes('--with-api'),
noLists: commandArguments.includes('--no-lists'),
let timeRunGetListArray = 0
let timeRunGetListByCategories = 0
function normalizeVersion ( rawVersion ) {
const containsNumbers = /\d+/.test( rawVersion )
if ( !containsNumbers ) {
return '0.0.0'
}
let version = rawVersion
// Parse each part
version = version
.split('.')
.map( part => {
// Trim leading zeros
return part.replace(/^0+/, '')
} )
.join('.')
return semver.coerce(version)
}
class BuildLists {
constructor () {
@ -59,57 +111,39 @@ class BuildLists {
// Main build methods
{
name: 'app',
endpointPrefix: 'app',
path: '/static/app-list.json',
buildMethod: buildAppList,
},
{
name: 'game',
endpointPrefix: 'game',
path: '/static/game-list.json',
buildMethod: buildGamesList,
},
{
name: 'homebrew',
endpointPrefix: 'formula',
path: '/static/homebrew-list.json',
buildMethod: buildHomebrewList,
},
{
name: 'device',
endpointPrefix: 'device',
path: '/static/device-list.json',
buildMethod: buildDeviceList,
},
// Secondary Derivative built lists
// Always goes after initial lists
// since it depend on them
// since it depends on them
{
name: 'video',
endpointPrefix: 'tv',
path: '/static/video-list.json',
buildMethod: async () => {
// console.log('this.getAllVideoAppsList()', this.getAllVideoAppsList())
return await buildVideoList( this.getAllVideoAppsList() )
// const videoList = await buildVideoList( this.getAllVideoAppsList() )
// const extraVideos = []
// const multiplier = 12
// for (let i = 0; i < multiplier; i++) {
// videoList.forEach( video => {
// extraVideos.push({
// ...video,
// slug: video.slug + '-' + i,
// })
// })
// }
// return new Set([
// ...videoList,
// ...extraVideos
// ].slice(0, 10 * 1000))
},
beforeSave: videoListSet => {
this.allVideoAppsList = this.getAllVideoAppsList()
@ -124,6 +158,28 @@ class BuildLists {
}
]
getListOptions ( listName ) {
return this.listsOptions.find( listOption => listOption.name === listName )
}
shouldHaveRelatedVideos ( app ) {
const appType = getAppType( app )
const typeWithRelatedVideos = new Set([
'app',
'formula',
'game'
])
return typeWithRelatedVideos.has( appType )
}
shouldHaveDeviceSupport ( app ) {
const appType = getAppType( app )
return appType === 'app' || appType === 'formula' || appType === 'game'
}
getAllVideoAppsList = () => {
return new Set([
...this.lists.app,
@ -131,6 +187,69 @@ class BuildLists {
])
}
bundles = []
async getSavedAppBundles ( options = {} ) {
const {
keepBundlesInMemory = true
} = options
if ( !keepBundlesInMemory ) {
// console.log('Getting bundles from file')
return await fs.readJson('./static/app-bundles.json')
}
// From here we get and store bundles
// Check if any bundles are already in memory
if ( this.bundles.length === 0 ) {
// console.log('Storing bundles to memory')
this.bundles = await fs.readJson('./static/app-bundles.json')
}
// console.log('Getting bundles from memory')
return this.bundles
}
// Load the bundles from files
// so that we don't have to keep them in memory
async findAppBundle ( needleBundleIdentifier ) {
const bundles = await this.getSavedAppBundles()
return bundles.find( ([
storedAppBundleIdentifier,
// versions
]) => storedAppBundleIdentifier === needleBundleIdentifier )
}
async getAppBundles ( app ) {
return await Promise.all( app.bundleIds.map( async bundleIdentifier => {
return await this.findAppBundle( bundleIdentifier )
} ) )
}
sortBundleVersions ( bundles ) {
return bundles.map( bundle => {
const [
bundleIdentifier,
versionsObject
] = bundle
// Sort versions by semver
const versions = Object.entries( versionsObject ).sort( ( [ aVersionRaw ], [ bVersionRaw ] ) => {
const aVersion = normalizeVersion( aVersionRaw )
const bVersion = normalizeVersion( bVersionRaw )
return semver.compare( bVersion, aVersion )
} )
return [
bundleIdentifier,
versions
]
} )
}
saveToJson = async function ( content, path ) {
// Write the list to JSON
@ -149,24 +268,21 @@ class BuildLists {
const hasSaveMethod = listOptions.hasOwnProperty('beforeSave')
const saveMethod = hasSaveMethod ? listOptions.beforeSave : listSet => Array.from( listSet )
// console.log('listFullPath', listFullPath)
const saveableList = saveMethod( this.lists[listOptions.name] )
console.log('saveableList', typeof saveableList)
// console.log('saveableList', typeof saveableList)
// Stringify one at a time to allow for large lists
const saveableListJSON = '[' + saveableList.map(el => JSON.stringify(el)).join(',') + ']'
// Write the list to JSON
await fs.writeFile(listFullPath, JSON.stringify( saveableList ))
await fs.writeFile(listFullPath, saveableListJSON)
// 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)
// Import the created JSON File
return savedList
return
}
// Run all listsOprions methods
@ -194,11 +310,152 @@ class BuildLists {
return
}
saveApiEndpoints = async function ( listOptions ) {
getListArray ( listName ) {
console.log(`getListArray run ${ timeRunGetListArray += 1 } times`)
await PromisePool
.withConcurrency(1000)
.for( Array.from( this.lists[listOptions.name] ) )
return Array.from( this.lists[ listName ] )
}
getListArrayMemoized = memoize( this.getListArray.bind( this ) )
makeAppsByCategory () {
// Intialize empty category lists
// so empty categories still get defined
const emptyCategories = Object.fromEntries(
Object.keys( categories ).map( categorySlug => [ categorySlug, [] ])
)
const appsByCategory = this.getListArrayMemoized( 'app' ).reduce( ( categories, app ) => {
const categorySlug = app.category.slug
if ( !categories[categorySlug] ) {
throw Error(`Category ${categorySlug} not defined`)
}
categories[categorySlug].push( app )
return categories
}, emptyCategories )
return appsByCategory
}
getAppsByCategory = this.makeAppsByCategory//memoize( this.makeAppsByCategory.bind( this ) )
getAppCategoryList ( category ) {
const categoryLists = this.getAppsByCategory()
return categoryLists[category]
}
makeKindLists () {
const getters = Object.fromEntries( Object.entries( this.lists ).map( ([ listName ]) => {
const listEndpointPrefix = this.getListOptions( listName ).endpointPrefix
return [
// Key
listEndpointPrefix,
() => this.getListArrayMemoized( listName )
]
} ) )
// Add getters for categories
// Homebrew category overrides Homebrew app type from above
for ( const categorySlug in categories ) {
// Throw if category already defined
if ( getters[categorySlug] ) throw Error(`Category ${categorySlug} already defined`)
getters[categorySlug] = () => this.getAppCategoryList( categorySlug )
}
const kindLists = {}
for ( const kindSlug in getters ) {
// Throw if kindSlug already defined
if ( kindLists[kindSlug] ) throw Error(`Kind ${kindSlug} already defined`)
kindLists[ kindSlug ] = new KindList({
// Get list method
list: getters[ kindSlug ],
kindSlug
})
}
return kindLists
}
get kindRoutes () {
return Object.entries( this.makeKindLists() ).map( ([ kindSlug, kindList ]) => {
return kindList.routes
}).flat()
}
async saveKinds () {
const kindLists = this.makeKindLists()
// Save the lists
for ( const kindSlug in kindLists ) {
console.log('\n', `-- Starting kind lists for ${ kindSlug }`)
const endpointMethodName = `Finished kind lists for ${ kindSlug }`
console.time(endpointMethodName)
const kindList = kindLists[ kindSlug ]
// Ensure the base directory exists
await fs.ensureDir( kindList.basePath )
for ( const file of kindList.apiFiles ) {
// console.log('kindPage.items', kindPage)
await this.saveToJson( file.content, file.path )
}
console.timeEnd(endpointMethodName)
console.log( '\n\n' )
}
const kindIndex = categories
// Delete no-category
delete kindIndex['no-category']
// Store sample names into kindIndex as description
for ( const categorySlug in kindIndex ) {
const kindName = getCategoryKindName( categorySlug )
// Skip empty categories
if ( kindLists[ kindName ].list.length === 0 ) continue
kindIndex[ categorySlug ].description = kindLists[ kindName ].summary.sampleNames
// Save kindName
kindIndex[ categorySlug ].kindName = kindName
}
// Save the index
await this.saveToJson( kindIndex, `${ apiDirectory }/kind/index.json` )
}
saveApiEndpoints = async ( listOptions ) => {
const apiListDirectory = `${ apiDirectory }/${ listOptions.endpointPrefix }`
const poolSize = 1000
// Store app bundles to memory
await this.getSavedAppBundles({
keepBundlesInMemory: true
})
const { errors } = await PromisePool
.withConcurrency( poolSize )
.for( this.getListArrayMemoized( listOptions.name ) )
.process(async ( listEntry, index, pool ) => {
// console.log('listEntry', listEntry)
@ -211,31 +468,71 @@ class BuildLists {
} = listEntry
const endpointPath = `./static/api${endpoint}.json`
const endpointPath = `${ apiDirectory }${ endpoint }.json`
const endpointDirectory = dirname(endpointPath)
// Stop if the endpoint is already exists
if (fs.existsSync(endpointPath)) {
console.log(`Path "${endpointPath}" already exists`)
return
// Add related videos
if ( this.shouldHaveRelatedVideos( listEntry ) ) {
listEntry.relatedVideos = getRelatedVideos({
listing: listEntry,
videoListSet: this.lists.video,
appListSet: this.allVideoAppsList
})
}
// console.log(`Saving endpoint "${endpoint}" to "${endpointPath}"`)
// Add App Bundles
if ( Array.isArray( listEntry.bundleIds ) ) {
listEntry.bundles = await this.getAppBundles( listEntry )
listEntry.bundles = this.sortBundleVersions( listEntry.bundles )
}
// Add device support
if ( this.shouldHaveDeviceSupport( listEntry ) ) {
const deviceList = this.getListArrayMemoized( 'device' )
listEntry.deviceSupport = deviceList.map( device => {
const supportsApp = deviceSupportsApp( device, listEntry )
return {
...device,
emoji: supportsApp ? '✅' : '🚫',
ariaLabel: `${ listEntry.name } has ${ supportsApp ? '' : 'not' } been reported to work on ${ device.name }`
}
})
}
const hasSaveMethod = has( listOptions, 'beforeSave' )
const saveMethod = hasSaveMethod ? listOptions.beforeSave : listSet => Array.from( listSet )
const [ saveableEntry ] = saveMethod( new Set( [ listEntry ] ) )
// Ensure the directory exists
await fs.ensureDir( endpointDirectory )
// Write the endpoint to JSON
await this.saveToJson( listEntry, endpointPath )
await this.saveToJson( saveableEntry, endpointPath )
})
if ( errors.length !== 0 ) {
throw new Error( errors )
}
// Save each enpoint's data
// for ( const listEntry of this.lists[listOptions.name] ) {
// Count saved files
const fileCount = fs.readdirSync( apiListDirectory ).length
console.log( fileCount, 'Files saved in', apiListDirectory )
console.log( this.lists[listOptions.name].size, 'Entries' )
if ( fileCount !== this.lists[listOptions.name].size ) {
const listSlugs = Array.from( this.lists[listOptions.name] ).map( listEntry => listEntry.slug )
const fileNames = fs.readdirSync( apiListDirectory ).map( fileName => basename(fileName).split('.')[0] )
logArraysDifference( listSlugs, fileNames )
throw new Error( `Files (${ fileCount }) don\'t match list count in ${ apiListDirectory }(${ this.lists[listOptions.name].size }).` )
}
// }
}
// Save app lists to JSON
@ -266,33 +563,53 @@ class BuildLists {
console.timeEnd(methodName)
if ( cliOptions.withApi ) {
console.log('Saving individual endpoints...')
console.log('\n', `-- Starting individual /${ listOptions.name } endpoints`)
const endpointMethodName = `Saved /${ listOptions.name } endpoints`
const endpointMethodName = `Finished individual /${ listOptions.name } endpoints`
console.time(endpointMethodName)
await this.saveApiEndpoints( listOptions )
console.timeEnd(endpointMethodName)
console.log( '\n\n' )
}
}
//
console.log('Save lists finished')
return
}
async saveAllAppsSummary () {
const summaryNumbers = getListSummaryNumbers( [
...this.getListArray('app'),
...this.getListArray('game'),
...this.getListArray('homebrew'),
] )
await this.saveToJson( summaryNumbers, `${apiDirectory}/all-apps-summary.json` )
return summaryNumbers
}
async build () {
await saveYouTubeVideos()
// Pull in and layer data from all sources
await this.buildLists()
// Save the data to respective files as lists
await this.saveAppLists()
await this.saveAllAppsSummary()
// Save kind lists
await this.saveKinds()
// console.log('appList', appList)
// console.log('this.allVideoAppsList', this.allVideoAppsList.length, this.allVideoAppsList[0])
// Add list based routes
for ( const listKey in this.lists ) {
@ -331,14 +648,12 @@ class BuildLists {
}
// Add standard app endpoint
if ( appType === 'app' || appType === 'formula' ) {
if ( this.shouldHaveRelatedVideos( app ) ) {
const relatedVideos = videosRelatedToApp( app, this.lists.video ).map(video => {
// console.log('video', video)
return {
...video,
endpoint: `${getAppEndpoint(app)}/benchmarks#${video.id}`
}
const relatedVideos = getRelatedVideos({
listing: app,
videoListSet: this.lists.video,
appListSet: this.allVideoAppsList
})
// Add app or formula endpoint
@ -373,33 +688,45 @@ class BuildLists {
this.endpointMaps.nuxt.set( '/kind/' + slug, {} )
})
// Save Nuxt Endpoints
// await this.saveToJson(Array.from(this.endpointMaps.nuxt), './static/nuxt-endpoints.json')
// // Save Eleventy Endpoints
// await this.saveToJson(Array.from(this.endpointMaps.eleventy), './static/eleventy-endpoints.json')
// console.log('this.endpointMaps.eleventy /app/chrome', this.endpointMaps.eleventy.get( '/app/chrome' ))
// Filter eleventy endpoints
// this.endpointMaps.eleventy = new Set([
// ['/app/chrome', this.endpointMaps.eleventy.get( '/app/chrome' )]
// ])
if ( !cliOptions.withApi ) {
for ( const [ endpointSetName, endpointSet ] of Object.entries(this.endpointMaps) ) {
// Save Endpoints
await this.saveToJson(Array.from( endpointSet , ([route, payload]) => ({ route, payload })), `./static/${endpointSetName}-endpoints.json`)
}
// Save sitemap endpoints
await this.saveToJson(Object.values(this.endpointMaps).map( endpointSet => {
return Array.from( endpointSet , ([route, payload]) => ({ route, payload }) )
} ).flat(1), './static/sitemap-endpoints.json')
for ( const [ endpointSetName, endpointSet ] of Object.entries(this.endpointMaps) ) {
// Save Endpoints
await this.saveToJson(Array.from( endpointSet , ([route, payload]) => ({ route, payload })), `./static/${endpointSetName}-endpoints.json`)
}
const sitemapEndpoints = Object.values(this.endpointMaps).map( endpointSet => {
return Array.from( endpointSet , ([route, payload]) => ({ route, payload }) )
} ).flat(1)
// Add kind routes to sitemap
this.kindRoutes.forEach( route => {
sitemapEndpoints.push({
route,
payload: {}
})
} )
// Add routes for Astro pages
const astroPageUrls = await getUrlsForAstroDefinedPages()
astroPageUrls.forEach( url => {
sitemapEndpoints.push({
route: url,
payload: {}
})
})
// Save sitemap endpoints
console.log('Building Sitemap JSON')
await this.saveToJson( sitemapEndpoints, './static/sitemap-endpoints.json')
// Save XML Sitemap
console.log('Building XML Sitemap')
await saveSitemap( sitemapEndpoints.map( ({ route }) => route ) )
// Save stork toml index
console.log('Building Stork toml index')
await writeStorkToml( sitemapEndpoints )
console.log('Total Nuxt Endpoints', this.endpointMaps.nuxt.size )
console.log('Total Eleventy Endpoints', this.endpointMaps.eleventy.size )
console.log('Total Endpoints', this.endpointMaps.nuxt.size + this.endpointMaps.eleventy.size )
@ -412,9 +739,3 @@ class BuildLists {
const listBuilder = new BuildLists()
listBuilder.build()
// export default async function () {
// const listBuilder = new BuildLists()
// return await listBuilder.build()
// }

23
build-stork.js Normal file
View file

@ -0,0 +1,23 @@
import fs from 'fs-extra'
import {
downloadStorkExecutable,
// writeStorkToml
} from './helpers/stork/toml.js'
;(async () => {
console.log( 'Downloading Stork executable...' )
await downloadStorkExecutable()
// console.log( 'Building Stork index TOML...' )
// Get Sitemap Endpoints JSON
// const sitemap = await fs.readJson( './static/sitemap-endpoints.json' )
// await writeStorkToml( sitemap )
// From here we hand off to the Stork executable
process.exit()
})()

View file

@ -60,6 +60,9 @@
<script>
import axios from 'axios'
import { v4 as uuid } from 'uuid'
import { isNuxt } from '~/helpers/environment.js'
export default {
props: {
@ -78,6 +81,10 @@ export default {
inputClassGroups: {
type: Object,
default: () => {}
},
uuid: {
type: String,
default: uuid()
}
},
data: function () {
@ -90,7 +97,7 @@ export default {
},
computed: {
inputId () {
return `all-updates-subscribe-${this._uid}`
return `all-updates-subscribe-${ this.uuid }`
},
inputClasslist () {
const defaultClassGroups = {
@ -115,6 +122,7 @@ export default {
return Object.values(mergedClassGroups)
}
},
methods: {
async trySubmit () {
console.log('Trying submit')
@ -124,10 +132,15 @@ export default {
// const pagePath = $nuxt.$route.path
// console.log('this.$config.allUpdateSubscribe', this.$config.allUpdateSubscribe)
const allUpdateSubscribe = isNuxt( this ) ? this.$config.allUpdateSubscribe : global.$config.allUpdateSubscribe
console.log('allUpdateSubscribe', allUpdateSubscribe)
// https://stackoverflow.com/questions/51995070/post-data-to-a-google-form-with-ajax/55496118#55496118
const actionUrl = this.$config.allUpdateSubscribe
const actionUrl = allUpdateSubscribe
console.log('actionUrl', actionUrl)
axios({
method: 'post',

View file

@ -1,14 +1,12 @@
<template>
<div class="doesitarm-carbon-wrapper">
<component
:is="'script'"
v-once
id="_carbonads_js"
:is="'script'"
src="https://cdn.carbonads.com/carbon.js?serve=CK7DVK3M&placement=doesitarmcom"
type="text/javascript"
async
class="include-on-static"
/>
</div>
</template>

View file

@ -29,7 +29,7 @@ export default {
links () {
return [
{
label: 'Scan Your Own App',
label: 'Scan Apps',
href: '/apple-silicon-app-test/'
},
{

View file

@ -87,7 +87,7 @@
:href="item.url"
:class="[
'mt-1 block px-3 py-2 rounded-md text-base font-medium text-gray-300 hover:text-white hover:bg-gray-700 focus:outline-none focus:text-white focus:bg-gray-700 transition duration-150 ease-in-out',
($nuxt.$route.path === item.url) ? 'text-white bg-gray-900 hover:text-white' : 'text-gray-300 hover:bg-gray-700'
(currentPath === item.url) ? 'text-white bg-gray-900 hover:text-white' : 'text-gray-300 hover:bg-gray-700'
]"
>
{{ item.label }}
@ -109,7 +109,7 @@
:href="item.url"
:class="[
'px-3 py-2 rounded-md text-sm font-medium leading-5 focus:outline-none focus:text-white focus:bg-gray-700 transition duration-150 ease-in-out',
($nuxt.$route.path === item.url) ? 'text-white bg-darker hover:text-white neumorphic-shadow' : 'text-gray-300 hover:bg-darker hover:neumorphic-shadow'
(currentPath === item.url) ? 'text-white bg-darker hover:text-white neumorphic-shadow' : 'text-gray-300 hover:bg-darker hover:neumorphic-shadow'
]"
>
{{ item.label }}
@ -141,7 +141,7 @@
<a
:class="[
'underline px-3 py-2 rounded-md text-xs font-medium leading-5 focus:outline-none focus:text-white focus:bg-gray-700 transition duration-150 ease-in-out',
//($nuxt.$route.path === item.url) ? 'text-white bg-darker hover:text-white neumorphic-shadow' : 'text-gray-300 hover:bg-darker hover:neumorphic-shadow'
//(currentPath === item.url) ? 'text-white bg-darker hover:text-white neumorphic-shadow' : 'text-gray-300 hover:bg-darker hover:neumorphic-shadow'
]"
href="https://prf.hn/l/7JG0bEj"
>
@ -162,6 +162,9 @@
</template>
<script>
// https://anguscroll.com/just/just-has
import has from 'just-has'
import LinkButton from '~/components/link-button.vue'
export default {
@ -203,6 +206,21 @@ export default {
])
}
},
computed: {
currentPath () {
// If we have a nuxt context, use that.
if ( has( this, [ '$nuxt' ]) ) {
return this.$nuxt.$route.path
}
// If we have a location object, use that.
if ( typeof window !== 'undefined' && typeof window.location === 'object' ) {
return window.location.pathname
}
return '/'
}
}
// data: function () {
// return {
// // isOpen: false

565
components/search-stork.vue Normal file
View file

@ -0,0 +1,565 @@
<template>
<div
ref="search-container"
class="search-container w-full space-y-4"
>
<slot name="before-search">
<div class="list-summary-wrapper flex justify-center text-center text-sm">
<ListSummary
v-if="summary !== null"
:custom-numbers="summary"
class="max-w-4xl"
/>
</div>
</slot>
<div class="search-input relative space-y-4">
<div>
<input
id="search"
ref="search"
v-model="userTextQuery"
:autofocus="autofocus"
aria-label="Type here to Search"
class="appearance-none w-full text-white font-hairline sm:text-5xl outline-none bg-transparent p-3"
type="search"
placeholder="Type to Search"
autocomplete="off"
@keyup="handleSearchInput"
>
<div class="search-input-separator border-white border-t-2" />
</div>
<div class="quick-buttons overflow-x-auto whitespace-no-wrap space-x-2">
<button
v-for="button in quickButtons"
:key="button.query"
:class="[
'inline-block text-xs rounded-lg py-1 px-2',
'border-2 border-white focus:outline-none',
hasActiveFilter( button.query ) ? 'border-opacity-50 bg-darkest' : 'border-opacity-0 neumorphic-shadow-inner'
]"
:aria-label="`Filter list by ${button.label}`"
@click="toggleFilter(button.query); queryResults(query)"
>{{ button.label }}</button>
</div>
</div>
<Carbon class="carbon-inline-wide" />
<div
ref="search-container"
class="search-container relative divide-y divide-gray-700 w-full rounded-lg border border-gray-700 bg-gradient-to-br from-darker to-dark my-8 px-5"
>
<svg style="display: none;">
<defs>
<path
id="chevron-right"
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"
/>
</defs>
</svg>
<!-- hasStartedAnyQuery: {{ hasStartedAnyQuery }} -->
<template v-if="chunkedListings.length === 0">
<div
class="text-center py-4"
>
<span>No apps found for</span>
<span
v-for="term in userTerms"
:key="term"
class="font-bold border rounded px-1 pb-1 mx-1"
>{{ term }}</span>
<template v-if="isFilteredList">
<span>within</span>
<span
v-for="term in baseFilters"
:key="term"
class="font-bold border rounded px-1 pb-1 mx-1"
>{{ term }}</span>
</template>
</div>
<div
v-if="baseFilters.length > 0"
class="w-full flex justify-center p-6"
>
<LinkButton
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="{
shadow: 'hover:neumorphic-shadow',
bg: 'hover:bg-darker',
}"
>
<span>Search everything</span>
<svg
class="h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
>
<use href="#chevron-right" />
</svg>
</LinkButton>
</div>
</template>
<ul
v-for="(listingsChunk, chunkIndex) in chunkedListings"
:key="`listings-chunk-${ chunkIndex }`"
class="listings-container divide-y divide-gray-700"
>
<!-- <pre>
{{ listingsChunk }}
</pre> -->
<li
v-for="(listing, listingIndex) in listingsChunk"
:key="`${ listing.slug }-${ listingIndex }`"
:ref="`${ listing.slug }-row`"
:data-app-slug="listing.slug"
class="relative"
>
<!-- app.endpoint: {{ app.endpoint }} -->
<a
:href="listing.endpoint"
: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',
listing?.linkClass
]"
style="transition-property: border;"
>
<div class="absolute hidden left-0 h-12 w-12 rounded-full md:flex items-center justify-center bg-darker">
{{ getIconForListing( listing ) }}
</div>
<h3
v-html="listing.name"
/>
<div class="text-sm leading-5 font-bold">
{{ listing.text }}
</div>
<div
v-if="listing.storkResult"
class="text-xs leading-5 font-light"
>
<div
v-for="(excerpt, excerptIndex) in listing.storkResult.excerpts"
:key="`excerpt-${ excerptIndex }`"
class="result-excerpt space-y-3"
>
<div
v-for="(range, rangeIndex) in makeHighlightedMarkup( excerpt )"
:key="`range-${ rangeIndex }`"
v-html="range"
/>
</div>
</div>
<!-- listing.lastUpdated: {{ listing.lastUpdated }} -->
<template v-if="listing.lastUpdated">
<small
class="text-xs opacity-50"
>
<RelativeTime
:timestamp="listing.lastUpdated.timestamp"
class="text-xs opacity-50"
/>
</small>
</template>
<!-- listing.endpoint: {{ listing.endpoint }} -->
</a>
<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-4">
<LinkButton
v-for="link in getSearchLinks( listing )"
:key="`${ listing.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-bold',
// 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
v-if="listing.endpoint.length"
:href="listing.endpoint"
: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-bold',
// 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>
<svg
class="h-5 w-5 -mr-2"
viewBox="0 0 20 20"
fill="currentColor"
>
<use href="#chevron-right" />
</svg>
</LinkButton>
</div>
</div>
</li>
<li
v-if="showingInitialList"
class="list-navigation"
>
<nav
class="pagination w-full flex gap-6 justify-center py-4"
>
<LinkButton
v-if="previousPageUrl"
:href="previousPageUrl"
:class="[
'w-32 justify-end',
'rounded-md text-sm pointer-events-auto focus:outline-none focus:text-white focus:bg-gray-700 transition duration-150 ease-in-out px-3 py-2',
'text-gray-300 hover:bg-darker hover:neumorphic-shadow'
]"
:class-groups="{
shadow: 'hover:neumorphic-shadow',
bg: 'hover:bg-darker',
}"
>
<svg
class="-scale-x-100 h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
>
<use href="#chevron-right" />
</svg>
<span>Previous</span>
</LinkButton>
<LinkButton
v-if="nextPageUrl"
:href="nextPageUrl"
:class="[
'w-32',
'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="{
shadow: 'hover:neumorphic-shadow',
bg: 'hover:bg-darker',
}"
>
<span>Next</span>
<svg
class="h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
>
<use href="#chevron-right" />
</svg>
</LinkButton>
</nav>
</li>
</ul>
</div>
<div class="list-end-area flex justify-center py-6">
<ListEndButtons
:query="userTextQuery"
/>
</div>
</div>
</template>
<script>
import scrollIntoView from 'scroll-into-view-if-needed'
import {
defaultStatusFilters,
} from '~/helpers/statuses.js'
import {
getIconForListing
} from '~/helpers/app-derived.js'
import {
StorkClient,
StorkFilters,
makeHighlightedMarkup,
makeHighlightedResultTitle
} from '~/helpers/stork/browser.js'
import Carbon from '~/components/carbon-inline.vue'
import LinkButton from '~/components/link-button.vue'
import RelativeTime from '~/components/relative-time.vue'
import ListSummary from '~/components/list-summary.vue'
import ListEndButtons from '~/components/list-end-buttons.vue'
let storkClient = null
export default {
components: {
Carbon,
ListSummary,
RelativeTime,
LinkButton,
ListEndButtons
},
props: {
kindPage: {
type: Object,
required: true
},
initialLimit: {
type: Number,
default: null
},
autofocus: {
type: Boolean,
default: false
},
quickButtons: {
type: Array,
default: () => defaultStatusFilters
},
baseFilters: {
type: Array,
default: () => []
},
listSummary: {
type: Object,
default: () => null
},
},
data: function () {
return {
userTextQuery: '',
filterQueryList: [],
hasStartedAnyQuery: false,
listingsResults: [],
waitingForQuery: false
}
},
computed: {
storkQuery () {
return [
this.userTextQuery.trim(),
...this.filterQueryList
].join(' ')
},
appList () {
return this.kindPage.items
},
initialList () {
return this.initialLimit !== null ? this.appList.slice(0, this.initialLimit) : this.appList
},
listings () {
// Build filler listings to use while loading results
if ( this.waitingForQuery ) return Array( 10 ).fill({ name: 'Loading', slug: 'loading', endpoint: '', linkClass: 'shimmer pointer-events-none' })
if ( this.showingInitialList ) return this.initialList
return this.listingsResults
},
// Chunk results to avoid having a parent node with more than 60 child nodes.
chunkedListings () {
const listings = [
...this.listings
]
const size = 25
const chunks = []
while (listings.length > 0)
chunks.push(listings.splice(0, size))
return chunks
},
isFilteredList () {
return this.baseFilters.length > 0
},
hasSearchInputText () {
return this.userTextQuery.length > 0
},
hasAnyUserFilters () {
return this.userFilters.length > 0
},
hasAnyUserTerms () {
return this.userTerms.length > 0
},
showingInitialList () {
return !this.hasAnyUserTerms
},
inputTerms () {
return this.userTextQuery.trim().split(' ')
},
userFilters () {
// console.log('filterQueryList', )
return this.filterQueryList.filter( filterTerm => {
return !this.baseFilters.includes( filterTerm )
})
},
userTerms () {
// If out input is empty, return just the user filters
if ( !this.hasSearchInputText ) return this.userFilters
return [
...this.inputTerms,
...this.userFilters
]
},
summary () {
if ( this.listSummary !== null ) {
return this.listSummary
}
if ( !!this.kindPage.summary ) {
return this.kindPage.summary
}
return null
},
previousPageUrl () {
if ( this.kindPage.previousPage.length === 0 ) return null
return this.kindPage.previousPage
},
nextPageUrl () {
if ( this.kindPage.nextPage.length === 0 ) return null
return this.kindPage.nextPage
}
},
mounted () {
// Setup stork client
storkClient = new StorkClient()
// Store filter instance
this.storkFilters = new StorkFilters()
// Add initial filters
this.storkFilters.setFromStringArray( this.baseFilters )
},
methods: {
makeHighlightedMarkup,
makeHighlightedResultTitle,
getIconForListing,
getSearchLinks (app) {
return app?.searchLinks || []
},
// Search tools
hasActiveFilter ( filter ) {
return this.filterQueryList.includes( filter )
},
toggleFilter ( newFilterQuery ) {
this.storkFilters.toggleFilter( newFilterQuery )
this.filterQueryList = this.storkFilters.list
},
scrollInputToTop () {
scrollIntoView(this.$refs['search-container'], {
block: 'start',
behavior: 'smooth'
})
},
// Called on input and when a filter is toggled
async queryResults ( rawQuery ) {
console.log( 'query', this.storkQuery )
// If our query is empty
// then bail
if ( this.storkQuery.trim().length === 0 ) return
this.waitingForQuery = true
this.$emit('update:query', rawQuery)
// Declare that at least one query has been made
this.hasStartedAnyQuery = true
// console.log('rawQuery', rawQuery)
const requiredTerms = this.storkQuery.split(' ')
const storkQuery = await storkClient.lazyQuery( this.storkQuery, requiredTerms )
// If the query response is empty
// then return
if ( storkQuery === null ) {
return
}
// console.log( 'storkQuery', storkQuery )
this.listingsResults = storkQuery.results.map( result => {
return {
name: makeHighlightedResultTitle( result ),
endpoint: result.entry.url,
slug: '',
category: {
slug: 'uncategorized'
},
storkResult: result
}
})
// Switch from loading state and reveal the results
this.waitingForQuery = false
// console.log('this.listingsResults', this.listingsResults)
},
handleSearchInput ( event ) {
const inputValue = event.target.value
this.scrollInputToTop()
this.queryResults( inputValue )
},
}
}
</script>

View file

@ -1,7 +1,8 @@
<template>
<small class="data-credit text-sm opacity-75 text-center mb-4">
<span>Data generously provided by </span>
<div
class="data-credit flex gap-1 justify-center text-xs opacity-75 text-center mb-4"
>
<span>Includes data generously provided by </span>
<span>
<a
href="https://twitter.com/__tosh"
@ -15,6 +16,5 @@
class="font-bold"
>Apple Silicon Games</a>
</span>
</small>
</div>
</template>

View file

@ -59,6 +59,10 @@
<div class="cover-bottom h-full">
<slot>
<!-- Default slot -->
</slot>
<slot name="cover-bottom">
<!-- Bottom -->
</slot>

86
helpers/api/client.js Normal file
View file

@ -0,0 +1,86 @@
// Based on GitHub Proxy demo
// https://gist.github.com/v1vendi/75d5e5dad7a2d1ef3fcb48234e4528cb
// Example uses:
// DoesItAPI.get() // GET /api
// DoesItAPI.apps.get() // GET /api/apps.json
// DoesItAPI.apps(7).get() // GET /api/apps/7.json
// DoesItAPI.apps(7).whatever.delete() // DELETE /api/apps/7/whatever.json
// DoesItAPI.apps.put({ whatever: 1 })
// GET /api/tiles/public/static/3/4/2.json?turn=37038&games=wot
// DoesItAPI.tiles.public.static(3)(4)(`${2}.json`).get({ turn: 37, games: 'wot' })
import axios from 'axios'
import { getApiUrl } from '~/helpers/url.js'
// const defaultFetchMethod = (...args) => console.log(...args) // mock
const defaultFetchMethod = async function (...args) {
return axios(...args)
.then( response => response.data )
.catch( error => {
console.error( error )
throw error
})
}
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods
const HTTP_METHODS = [
'GET',
// 'POST',
// 'PUT',
// 'DELETE',
// 'PATCH'
]
const apiBaseUrl = `${ getApiUrl().replace(/\/$/, '') }/api`
export function generateAPI ( {
apiUrl = apiBaseUrl,
fetchMethod = defaultFetchMethod
} = {} ) {
// console.log( 'apiUrl', apiUrl )
// a hack, so we can use field either as property or a method
const callable = () => {}
callable.url = apiUrl
return new Proxy(callable, {
get({ url }, propKey) {
// If we're just getting the url, return it
if ( propKey === 'url' ) return `${ url }.json`
// If we're using an HTTP method
// then do a request to the url
if ( HTTP_METHODS.includes(propKey.toUpperCase()) ) {
return (data) => fetchMethod({
url: `${ url }.json`,
method: propKey.toUpperCase(),
data
})
}
// Otherwise drill down to the next property
// Example:
// From DoesItAPI.kind...
// To DoesItAPI.kind.apps...
return generateAPI({ apiUrl: `${url}/${propKey}` })
},
// Handles when () goes after a property key
// Example: DoesItAPI() or DoesItAPI.app()
// Proxy.handler.apply - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/apply
apply({ url }, thisArg, [arg] = []) {
const apiUrl = arg ? `${url}/${arg}` : url
return generateAPI({ apiUrl: apiUrl })
}
})
}
export const DoesItAPI = generateAPI()

1
helpers/api/config.js Normal file
View file

@ -0,0 +1 @@
export const apiDirectory = './static/api'

128
helpers/api/kind.js Normal file
View file

@ -0,0 +1,128 @@
// import memoize from 'fast-memoize'
import memoizeGetters from 'memoize-getters'
import getListSummaryNumbers from '~/helpers/get-list-summary-numbers.js'
import {
apiDirectory
} from '~/helpers/api/config.js'
import {
PaginatedList,
defaultPerPage
} from '~/helpers/api/pagination.js'
import {
makeSummaryOfListings
} from '~/helpers/categories.js'
const defaultExludedProperties = [
'bundles',
]
function excludeExtaKindData ( { rawKindPage, excludes = defaultExludedProperties } = {} ) {
return {
...rawKindPage,
items: rawKindPage.items.map( item => {
for ( const exclude of excludes ) {
delete item[ exclude ]
}
return item
})
}
}
function makeKindEndpoint ({ kindSlug, number = null }) {
if ( number ) {
return `/kind/${ kindSlug }/${ number }`
}
return `/kind/${ kindSlug }`
}
function makeKindDirPath ( kindSlug ) {
return `${ apiDirectory }${ makeKindEndpoint({ kindSlug }) }`
}
function makeKindFilePath ({ kindSlug, number }) {
return `${ apiDirectory }${ makeKindEndpoint({ kindSlug, number }) }.json`
}
// let timeSummaryRun = 0
export class KindList extends PaginatedList {
constructor({
list,
kindSlug,
perPage = defaultPerPage
}) {
super({ list, perPage })
this.kindSlug = kindSlug
}
baseRoute = makeKindEndpoint({ kindSlug: this.kindSlug })
makeSummary () {
// console.log( `Summary run ${ timeSummaryRun += 1 } times` )
return {
...getListSummaryNumbers( this.list ),
sampleNames: makeSummaryOfListings({ list: this.list }),
sampleNamesShort: makeSummaryOfListings({
list: this.list,
length: 5,
random: true,
suffix: ''
}),
}
}
summary = this.makeSummary()
get routes () {
return this.pages.map( kindPage => {
return makeKindEndpoint({
kindSlug: this.kindSlug,
number: kindPage.number
})
})
}
get basePath () {
return makeKindDirPath( this.kindSlug )
}
get apiFiles () {
return this.pages.map( kindPage => {
// If we have a number, we need to add it to the file path
const nextPage = kindPage.hasNextPage ? makeKindEndpoint({
kindSlug: this.kindSlug,
number: kindPage.number + 1
}) : ''
const previousPage = kindPage.hasPreviousPage ? makeKindEndpoint({
kindSlug: this.kindSlug,
number: kindPage.number - 1
}) : ''
return {
path: makeKindFilePath({ kindSlug: this.kindSlug, number: kindPage.number }),
content: {
number: kindPage.number,
previousPage,
nextPage,
summary: this.summary,
items: excludeExtaKindData({
rawKindPage: kindPage,
}).items
}
}
})
}
}
export const KindListMemoized = memoizeGetters( KindList )

76
helpers/api/pagination.js Normal file
View file

@ -0,0 +1,76 @@
export const defaultPerPage = 20
export class PaginatedList {
constructor({ list, perPage = defaultPerPage }) {
// Catch errors if the list is not an array or a function
if ( !Array.isArray( list ) && typeof list !== 'function' ) {
throw new Error(`List must be an array or a function but is ${typeof list}`)
}
this.listArg = list
this.perPage = perPage
}
get list () {
// if our list is a function, call it
if ( typeof this.listArg === 'function' ) {
return this.listArg()
}
return this.listArg
}
get total () {
// Catch errors if the list is not an array or a function
if ( !Array.isArray( this.list ) ) {
throw new Error(`List must be an array or a function but is ${typeof list}`)
}
return this.list.length
}
get pageCount () {
return Math.ceil( this.total / this.perPage )
}
makePageItems ( pageNumber ) {
const start = (pageNumber - 1) * this.perPage
const end = start + this.perPage
return this.list.slice(start, end)
}
hasPage ( pageNumber ) {
return pageNumber > 0 && pageNumber <= this.pageCount
}
makePage ( pageNumber ) {
const items = this.makePageItems( pageNumber )
return {
number: pageNumber,
items,
hasPreviousPage: this.hasPage( pageNumber - 1 ),
hasNextPage: this.hasPage( pageNumber + 1 ),
get json() {
return JSON.stringify( items )
}
}
}
get pages () {
// Create an empty array of pages
const pages = Array( this.pageCount ).fill({})
return pages.map( ( _, index ) => {
const pageNumber = index + 1
return this.makePage( pageNumber )
})
}
}

View file

@ -0,0 +1,54 @@
import glob from 'fast-glob'
import { simpleSitemapAndIndex } from 'sitemap'
import {
sitemapLocation
} from '~/helpers/constants.js'
import { getSiteUrl } from '~/helpers/url.js'
const astroPageTemplatePath = './src/pages'
export async function getUrlsForAstroDefinedPages () {
const siteUrl = getSiteUrl()
const filesPaths = await glob( `${ astroPageTemplatePath }/**/*.astro` )
const urls = []
for ( const filePath of filesPaths ) {
const urlPath = filePath
.replace( astroPageTemplatePath, '' )
.replace( '.astro', '' )
.replace( '/index', '/' )
// Skip any paths for templates that include '['
if ( urlPath.includes( '[' ) ) continue
console.log( 'urlPath', urlPath )
// Create a new url object from the site url and the path
const url = new URL( urlPath, siteUrl )
urls.push( url.pathname )
}
return urls
}
export async function saveSitemap ( sitemapUrls ) {
await simpleSitemapAndIndex({
hostname: process.env.PUBLIC_URL,
destinationDir: sitemapLocation,
gzip: false,
// [{ url: '/page-1/', changefreq: 'daily'}, ...],
sourceData: sitemapUrls.map( url => {
return {
url,
// Google doesn't care about changefreq and priority - https://www.seroundtable.com/google-priority-change-frequency-xml-sitemap-20273.html
// changefreq: 'daily'
}
}),
})
}

View file

@ -0,0 +1,73 @@
import path from 'path'
import fs from 'fs-extra'
import axios from 'axios'
import { parse } from 'fast-xml-parser'
import {
sitemapLocation,
sitemapIndexFileName,
} from '~/helpers/constants.js'
const sitemapFilesToTry = [
sitemapIndexFileName,
'sitemap.xml'
]
export function parseSitemapXml ( sitemapXml ) {
// Get URLs from index
const sitemapRoot = parse( sitemapXml )
const {
sitemapindex = null,
urlset = null,
} = sitemapRoot
if ( sitemapindex !== null ) {
const {
sitemap
} = sitemapindex
const urlEntries = Array.isArray( sitemap ) ? sitemap : [ sitemap ]
return urlEntries
}
// console.log( 'sitemapRoot', sitemapRoot )
return urlset.url
}
export async function getAllUrlsFromLocalSitemap ( sitemapPath ) {
// Get intial sitemap
const sitemapXml = await fs.readFile( sitemapPath, 'utf8' )
const sitemapDirectory = path.dirname( sitemapPath )
// Get URLs from index
const urlEntries = parseSitemapXml( sitemapXml )
// Check if url entries are sitemaps
const isSitemapIndex = !!urlEntries[0].loc && urlEntries[0].loc.includes('.xml')
if ( !isSitemapIndex ) return urlEntries
// Get urls from our sitemap
const sitemaps = await Promise.all( urlEntries.map( async entry => {
// Build Sitemap Index URL
const sitemapUrl = new URL( entry.loc )
const childSitemapPath = path.join( sitemapDirectory, sitemapUrl.pathname )
return await getAllUrlsFromLocalSitemap( childSitemapPath )
}))
// Flatten array
return sitemaps.flat()
}
export async function fetchParsedSitemapXmlForDomain ( domain ) {
for ( const sitemapFile of sitemapFilesToTry ) {
}
}

34
helpers/api/static.js Normal file
View file

@ -0,0 +1,34 @@
import fs from 'fs-extra'
import axios from 'axios'
import 'dotenv/config'
import {
// storkVersion,
// storkExecutableName,
// storkExecutablePath,
storkTomlPath,
} from '~/helpers/stork/config.js'
export async function downloadStorkToml () {
// Check if the toml file exists
if (fs.existsSync(storkTomlPath)) {
console.log(`Stork toml file already exists at ${storkTomlPath}`)
return
}
const apiUrl = new URL( process.env.PUBLIC_API_DOMAIN )
apiUrl.pathname = storkTomlPath.replace('static/', '')
const response = await axios({
method: "get",
url: apiUrl.toString(),
})
await fs.writeFile( storkTomlPath, response.data, { encoding: null })
// Get toml file stats
const stats = await fs.stat( storkTomlPath )
console.log( stats.isFile() ? '✅' : '❌', 'TOML is file', storkTomlPath )
// console.log('TOML Stats', stats)
}

View file

@ -0,0 +1,172 @@
import fs from 'fs-extra'
import { google } from 'googleapis'
import { playlists, benchmarksPlaylistId } from './playlists.js'
export const youtubeVideoPath = './static/api/youtube-videos.json'
async function getPlaylistsItems ( { playlistId } = {} ) {
const perPage = 50
// Setup Youtube API V3 Service instance
const service = google.youtube('v3')
// Fetch data from the Youtube API
const { errors = null, data = null } = await service.playlistItems.list({
key: process.env.GOOGLE_API_KEY,
part: 'snippet,contentDetails',
playlistId,
maxResults: perPage
}).catch(({ errors }) => {
console.log('Error fetching playlist', errors)
return {
errors
}
})
// Send an error response if something went wrong
if (errors !== null) {
throw new Error(errors)
return
}
const items = data.items
// If there are more results then push them to our playlist
if (data.nextPageToken !== null) {
// Store the token for page #2 into our variable
let pageToken = data.nextPageToken
while (pageToken !== null) {
// Fetch data from the Youtube API
const youtubePageResponse = await service.playlistItems.list({
key: process.env.GOOGLE_API_KEY,
part: 'snippet,contentDetails',
playlistId,
maxResults: perPage,
pageToken: pageToken
})
// Add the videos from this page on to our total items list
youtubePageResponse.data.items.forEach(item => items.push(item))
// Now that we're done set up the next page token or empty out the pageToken variable so our loop will stop
pageToken = ('nextPageToken' in youtubePageResponse.data) ? youtubePageResponse.data.nextPageToken : null
}
}
console.log(`Fetched ${items.length} videos from https://www.youtube.com/playlist?list=${ playlistId }`)
return items
}
async function getYouTubeVideos ( options = {} ) {
const {
// requestsDelay = 3600,
} = options
// Fetch all videos from playlists
const playlistSets = []
for ( const playlistToFetch of playlists ) {
// console.log('playlistJsonUrl', playlistJsonUrl)
const playlistItems = await getPlaylistsItems({
playlistId: playlistToFetch.id
})
// console.log('playlistItems', playlistItems.length)
playlistSets.push( playlistItems )
}
// Pull benchmarksPlaylist out of playlist sets
// benchmarksPlaylistId
const benchmarksVideoIds = playlistSets.find( playlist => {
// Skip empty playlists
if (playlist.length === 0) return false
// Get this playlist's ID from first video
// and check against benchmarksPlaylistId
return playlist[0].snippet.playlistId === benchmarksPlaylistId
}).map( video => video.contentDetails.videoId)
// Creat an object to store playlist items
const playlistItems = {}
// Loop through the sets and store all the videos into a single array
for (const playlistSet of playlistSets) {
for (const playlistItem of playlistSet) {
// If we've already stored this video
// then skip
if (playlistItems.hasOwnProperty(playlistItem.contentDetails.videoId)) continue
const tags = []
// If this video is in the benchmarks playlist
// then add the benchmark tag
if (benchmarksVideoIds.includes(playlistItem.contentDetails.videoId)) {
tags.push('benchmark')
}
// Store newly found video
playlistItems[playlistItem.contentDetails.videoId] = {
title: playlistItem.snippet.title,
description: playlistItem.snippet.description,
timestamps: [],
rawData: playlistItem,
tags
}
}
}
// Loop through playlist items and store timestamp data
for (const videoId in playlistItems) {
// console.log('playlistItem', playlistItem)
// If the description is empty
// then skip
if (playlistItems[videoId].description.trim().length === 0) continue
// Break up the description by line breaks
const descriptionLines = playlistItems[videoId].description.split(/\r?\n/)
// console.log('descriptionLines', descriptionLines)
for (const line of descriptionLines) {
// https://stackoverflow.com/a/11067610/1397641
const matches = line.match(/(?:([0-5]?[0-9]):)?([0-5]?[0-9]):([0-5][0-9])/)
// If there are no timestamps on this line
// then skip
if (matches === null) continue
playlistItems[videoId].timestamps.push({
time: matches[0],
fullText: line
})
}
}
return playlistItems
}
export async function saveYouTubeVideos () {
//
const youtubeVideos = await getYouTubeVideos()
//
// Save to JSON
await fs.outputJson( youtubeVideoPath, youtubeVideos )
}

View file

@ -0,0 +1,221 @@
export const benchmarksPlaylistId = 'PLaa9cZC07ZPFqjoYLZRR3kbbnJRHhlmXq'
export const playlists = [
// Awais Mirza - Apple Silicon Mac Software Testing
// https://www.youtube.com/playlist?list=PLz5rnvLVJX5XF8cXAOQuarPIeP8Xr7b1_
{
id: 'PLz5rnvLVJX5XF8cXAOQuarPIeP8Xr7b1_'
},
// Andrew Tsai - M1 Apple Silicon Game Benchmarks
// https://www.youtube.com/playlist?list=PLFbqxkNlqrHNK0i4WN99Jc8g-qSbKoZEy
{
id: 'PLFbqxkNlqrHNK0i4WN99Jc8g-qSbKoZEy'
},
// Andrew Tsai - Source Ports M1 Mac
// https://www.youtube.com/playlist?list=PLYzT4cEhhFvg5BPfy5-cOUNq7PtqzPr5s
{
id: 'PLYzT4cEhhFvg5BPfy5-cOUNq7PtqzPr5s'
},
// Andrew Tsai - Emulators M1 Mac
// https://www.youtube.com/playlist?list=PLYzT4cEhhFvh9IAsuic4paA_elCqNKs3J
{
id: 'PLYzT4cEhhFvh9IAsuic4paA_elCqNKs3J'
},
// Max Tech - Apple Silicon Macs Explained
// https://www.youtube.com/playlist?list=PLo11Rczpzuj05que94HF80LWD217ToJht
{
id: 'PLo11Rczpzuj05que94HF80LWD217ToJht'
},
// MrMacRight - Gaming Performance Tests
// https://www.youtube.com/playlist?list=PL9H5Z-IdZ8M0tUdSfS_rmdc8CRR_FD83e
{
id: 'PL9H5Z-IdZ8M0tUdSfS_rmdc8CRR_FD83e'
},
// Created Labs - New 2020 M1 MacBook
// https://www.youtube.com/playlist?list=PLcbQnQIwz6qSt-qsD3jCJaEw9fK8yXarA
{
id: 'PLcbQnQIwz6qSt-qsD3jCJaEw9fK8yXarA'
},
// DaVinci Resolve + Apple M1 Tests - Learn Color Grading
// https://www.youtube.com/playlist?list=PLRYMmqUFQ_cfETgJ_9tSHT1eD8c94y-qg
{
id: 'PLRYMmqUFQ_cfETgJ_9tSHT1eD8c94y-qg'
},
// Apple Silicon Macs — M1 & Beyond! - Rene Ritchie
// https://www.youtube.com/playlist?list=PL3XJJi5sAjD1sCicSKd0irf5TnQ1KJvPm
{
id: 'PL3XJJi5sAjD1sCicSKd0irf5TnQ1KJvPm'
},
// M1 Pro / Max — High-Performance Macs! - Rene Ritchie
// https://www.youtube.com/playlist?list=PL3XJJi5sAjD1U4yadF-wNFS-k34L11jqp
{
id: 'PL3XJJi5sAjD1U4yadF-wNFS-k34L11jqp'
},
// Apple Silicon Deep Dives — A15 to M1 Pro & Max! - Rene Ritchie
// https://www.youtube.com/playlist?list=PL3XJJi5sAjD3IOyW5k_CObxQse3JlPI4A
{
id: 'PL3XJJi5sAjD3IOyW5k_CObxQse3JlPI4A'
},
// Apple Silicon Macs — M1, Pro, & Max! - Rene Ritchie
// https://www.youtube.com/playlist?list=PL3XJJi5sAjD3kMEU_xUQLRHAt4_2r3UtF
{
id: 'PL3XJJi5sAjD3kMEU_xUQLRHAt4_2r3UtF'
},
// Apple M1 Silicon Benchmarks - Tonyisgaming
// https://www.youtube.com/playlist?list=PLgz-h_Uy9AwOctqRcm6hEYEqTO9yIawoO
{
id: 'PLgz-h_Uy9AwOctqRcm6hEYEqTO9yIawoO'
},
// M1 - Jerry Schulze
// https://www.youtube.com/playlist?list=PLFf7t8YdWQOgVmQdiEey2c_7ygDeZGBvf
{
id: 'PLFf7t8YdWQOgVmQdiEey2c_7ygDeZGBvf'
},
// Apple Silicon - DevChannel
// https://www.youtube.com/playlist?list=PLEi3_qsqIyclc-3NqbEYlIvXEdcLXy1qX
{
id: 'PLEi3_qsqIyclc-3NqbEYlIvXEdcLXy1qX'
},
// Michael P. Schmidt
// https://www.youtube.com/playlist?list=PLsT75DpPtn2N0RvXGdkjnwAYM0m9EMpb7
{
id: 'PLsT75DpPtn2N0RvXGdkjnwAYM0m9EMpb7'
},
// Ben G. Kaiser
// https://www.youtube.com/playlist?list=PL_9qmWdi19yCOCzAdTfFxpZtXonyfWTYJ
{
id: 'PL_9qmWdi19yCOCzAdTfFxpZtXonyfWTYJ'
},
// Constant Geekery
// https://youtube.com/playlist?list=PLQncOO7KICBIH-1Jv3fhOOU9b3-j6XoED
{
id: 'PLQncOO7KICBIH-1Jv3fhOOU9b3-j6XoED'
},
// Alexander Ziskind
// https://youtube.com/playlist?list=PLPwbI_iIX3aR88msMh-cHoJiBqS6YMUUH
{
id: 'PLPwbI_iIX3aR88msMh-cHoJiBqS6YMUUH'
},
// Execute Automation
// https://youtube.com/playlist?list=PL6tu16kXT9Pqwg2H8G3mROh5g7LISl8wT
{
id: 'PL6tu16kXT9Pqwg2H8G3mROh5g7LISl8wT'
},
// Portland CNC
// https://youtube.com/playlist?list=PLlQPaN85gB1kRnc8RUnV-TEOXU_2iap8S
{
id: 'PLlQPaN85gB1kRnc8RUnV-TEOXU_2iap8S'
},
// Ben Designs
// https://youtube.com/playlist?list=PLQgB1FZhNI7XAf9-2Gb88o6MxxbqDQIgj
{
id: 'PLQgB1FZhNI7XAf9-2Gb88o6MxxbqDQIgj'
},
// Ben Aqua
// https://youtube.com/playlist?list=PLvswiYwf1PZR_V1cXgKu0fKqN690LXRal
{
id: 'PLvswiYwf1PZR_V1cXgKu0fKqN690LXRal'
},
// Tech Gear Talk
// https://youtube.com/playlist?list=PL9Y8hNm43poLPdLAfFikiO_c4ZvpB4Wi8
{
id: 'PL9Y8hNm43poLPdLAfFikiO_c4ZvpB4Wi8'
},
// c0pist
// https://youtube.com/playlist?list=PLmEmhiFYCJygSGcPGNRgiJ9T1AKxJjERI
{
id: 'PLmEmhiFYCJygSGcPGNRgiJ9T1AKxJjERI'
},
// BilValentine
// https://youtube.com/playlist?list=PLj__zPOcS7bkamYx2oe9p6YOn-63Imexd
{
id: 'PLj__zPOcS7bkamYx2oe9p6YOn-63Imexd'
},
// Techkhamun
// https://youtube.com/playlist?list=PLprAMxVeEzkb4-19ncZ6GNWWL4QMxZIQK
{
id: 'PLprAMxVeEzkb4-19ncZ6GNWWL4QMxZIQK'
},
// iCave
// https://youtube.com/playlist?list=PLXHx58X9BZxJfSmttXQJv1zmNi0f7ANhq
{
id: 'PLXHx58X9BZxJfSmttXQJv1zmNi0f7ANhq'
},
// Douglas Hewitt
// https://youtube.com/playlist?list=PLugJ1ygKKMlI4WwyCbdLJOOHUZkFwvYbK
{
id: 'PLugJ1ygKKMlI4WwyCbdLJOOHUZkFwvYbK'
},
// Painfully Honest Tech
// https://youtube.com/playlist?list=PLWrpcd0vRa5StCaJoGqT4NsmvOPjF-VZu
{
id: 'PLWrpcd0vRa5StCaJoGqT4NsmvOPjF-VZu'
},
// IrixGuy
// https://youtube.com/playlist?list=PLzbToXWOz_ZiL5rUDKGaTQS5-eeWfMq7T
{
id: 'PLzbToXWOz_ZiL5rUDKGaTQS5-eeWfMq7T'
},
// sand0m1ze gaming
// https://youtube.com/playlist?list=PLPPMLgyyaMusoENI6yomC6DsYUymf2ey5
{
id: 'PLPPMLgyyaMusoENI6yomC6DsYUymf2ey5'
},
// The Dev
// https://youtube.com/playlist?list=PLtOOsLeNlKexKL9yzwp7vpbXaaynmfuQb
{
id: 'PLtOOsLeNlKexKL9yzwp7vpbXaaynmfuQb'
},
// Luke Barousse - Data Science
// https://youtube.com/playlist?list=PL_CkpxkuPiT9eGORxdWVWn0J58AUki_0W
{
id: 'PL_CkpxkuPiT9eGORxdWVWn0J58AUki_0W'
},
// AudioMap
// https://youtube.com/playlist?list=PL2xBqm1csOKoBkzu4GCnlHe_KiTGjzlNL
{
id: 'PL2xBqm1csOKoBkzu4GCnlHe_KiTGjzlNL'
},
// Mark Payne
// https://youtube.com/playlist?list=PL_NaeOyKxj28b_iVaiXfsyPaKwVeht-mG
{
id: 'PL_NaeOyKxj28b_iVaiXfsyPaKwVeht-mG'
},
// White Sea Studio
// https://youtube.com/playlist?list=PLil8shFjsBGaDDxkXhXPYa937D3LcMJDx
{
id: 'PLil8shFjsBGaDDxkXhXPYa937D3LcMJDx'
},
// Pete Herro
// https://youtube.com/playlist?list=PLy7gjSbRTE7M8QuYN6Uxp81whm2gAfwK9
{
id: 'PLy7gjSbRTE7M8QuYN6Uxp81whm2gAfwK9'
},
// ProgramHub - M1 Pro / M1 Max Apple Silicon
// https://www.youtube.com/playlist?list=PLv-goqr3JHiT1_th-4G7KYt2ANT_nhN14
{
id: 'PLv-goqr3JHiT1_th-4G7KYt2ANT_nhN14'
},
// ProgramHub - MacBook M2
// https://www.youtube.com/playlist?list=PLv-goqr3JHiRlly49iSp69J-0eEeIBEXu
{
id: 'PLv-goqr3JHiRlly49iSp69J-0eEeIBEXu'
},
// My Personal Benchmarks Playlist
// https://www.youtube.com/playlist?list=PLaa9cZC07ZPFqjoYLZRR3kbbnJRHhlmXq
{
id: benchmarksPlaylistId
},
// My Personal Playlist (For odds and ends)
// https://www.youtube.com/playlist?list=PLaa9cZC07ZPGM1f6A3F72qXNtPbVLl4V7
{
id: 'PLaa9cZC07ZPGM1f6A3F72qXNtPbVLl4V7'
},
]

View file

@ -1,5 +1,10 @@
// App Data that is derived from other app data
import {
categories,
categoryTemplate
} from '~/helpers/categories.js'
export function isDevice ( listing ) {
if ( !listing.hasOwnProperty('endpoint') ) return false
@ -57,9 +62,32 @@ export function getVideoEndpoint ( video ) {
}
export function getRouteType ( routeString ) {
// Catch non-string routes
if ( typeof routeString !== 'string' ) {
console.warn( 'routeString is not a string', routeString )
throw new Error('Route is not a string')
}
// Remove first slash and split by remaining
// slashes to get first part of route
const [ routeType ] = routeString.substring(1).split('/')
const [ routeType, , subType = null ] = routeString.substring(1).split('/')
if ( subType === 'benchmarks' ) return 'benchmarks'
return routeType
}
export function getIconForListing ( listing ) {
const routeType = getRouteType( listing.endpoint )
if ( routeType === 'tv' || routeType === 'benchmarks' ) return '▶️'
if ( routeType === 'device' ) return '🖥'
if ( routeType === 'formula' ) return categories.homebrew.icon
if ( routeType === 'game' ) return categories.games.icon
// Just use default icon
return categoryTemplate.icon
}

View file

@ -3,8 +3,7 @@ import axios from 'axios'
import { isString } from './check-types.js'
import parseMacho from './macho/index.js'
const prettyBytes = require('pretty-bytes')
import prettyBytes from 'pretty-bytes'
const knownArchiveExtensions = new Set([
@ -38,31 +37,55 @@ function callWithTimeout(timeout, func) {
})
}
let zip
// https://stackoverflow.com/a/35610685/1397641
const arrayChangeHandler = {
get: function( target, property ) {
console.log('getting ' + property + ' for ' + target)
// property is index in this case
return target[property]
},
set: function( target, property, value, receiver ) {
console.log('setting ' + property + ' for ' + target + ' with value ' + value)
target[property] = value
// you have to return true to accept the changes
return true
}
}
export function makeObservableArray () {
const originalArray = []
const proxyToArray = new Proxy( originalArray, arrayChangeHandler )
return {
originalArray,
proxyToArray
}
}
let zip = null
export default class AppFilesScanner {
constructor( {
observableFilesArray,
testResultStore
testResultStore,
zipModule = null
} ) {
// Files to process
this.files = observableFilesArray
this.testResultStore = testResultStore
// https://gildas-lormeau.github.io/zip.js/
zip = require('@zip.js/zip.js')
// https://gildas-lormeau.github.io/zip.js/core-api.html#configuration
zip.configure({
workerScripts: true,
// workerScripts: {
// inflate: ["lib/z-worker-pako.js", "pako_inflate.min.js"]
// }
})
this.zipModule = zipModule
}
get zip () {
if ( this.zipModule ) return this.zipModule
return zip
}
isApp ( file ) {
@ -116,7 +139,9 @@ export default class AppFilesScanner {
// }
async unzipFile ( file ) {
const fileReader = new zip.BlobReader( file.instance )//new FileReader()
if ( !this.zip ) throw new Error('Zip module not loaded')
const fileReader = new this.zip.BlobReader( file.instance )//new FileReader()
fileReader.onload = function() {
@ -146,7 +171,7 @@ export default class AppFilesScanner {
// console.log('fileReader', fileReader)
// https://gildas-lormeau.github.io/zip.js/core-api.html#zip-reading
const zipReader = new zip.ZipReader( fileReader )
const zipReader = new this.zip.ZipReader( fileReader )
// zipReader.onprogress = console.log
@ -404,7 +429,7 @@ export default class AppFilesScanner {
const infoXml = await rootInfoEntry.getData(
// writer
// https://gildas-lormeau.github.io/zip.js/core-api.html#zip-writing
new zip.TextWriter(),
new this.zip.TextWriter(),
// options
{
useWebWorkers: true,
@ -478,7 +503,7 @@ export default class AppFilesScanner {
const bundleExecutableBlob = await bundleExecutable.getData(
// writer
// https://gildas-lormeau.github.io/zip.js/core-api.html#zip-writing
new zip.BlobWriter(),
new this.zip.BlobWriter(),
// options
{
useWebWorkers: true
@ -595,4 +620,32 @@ export default class AppFilesScanner {
return
}
async setupZipReader () {
// https://gildas-lormeau.github.io/zip.js/
zip = await import('@zip.js/zip.js')
// console.log( 'zip', zip )
// https://gildas-lormeau.github.io/zip.js/core-api.html#configuration
zip.configure({
workerScripts: true,
// workerScripts: {
// inflate: ["lib/z-worker-pako.js", "pako_inflate.min.js"]
// }
})
}
get isSetup () {
return this.zip === null
}
async setup () {
// Setup zip reader if not already done
if ( !this.zipModule && !zip ) {
await this.setupZipReader()
}
}
}

21
helpers/array.js Normal file
View file

@ -0,0 +1,21 @@
export function getSymmetricDifference (a, b) {
return [
a.filter(x => !b.includes(x)),
b.filter(x => !a.includes(x))
]
}
export function logArraysDifference (a, b) {
const [ aOnly, bOnly ] = getSymmetricDifference(a, b)
console.log( 'Missing from first list:', aOnly )
console.log( 'Missing from second list:', bOnly )
console.log( `List difference Count ${ aOnly.length } / ${ bOnly.length }`, )
return {
aOnly,
bOnly,
}
}

17
helpers/astro/request.js Normal file
View file

@ -0,0 +1,17 @@
import { getNetlifyRedirect } from '~/helpers/config-node.js'
export async function catchRedirectResponse ( Astro ) {
const requestUrl = new URL( Astro.request.url )
const netlifyRedirectUrl = await getNetlifyRedirect( requestUrl.pathname )
// console.log('netlifyRedirectUrl', netlifyRedirectUrl)
if ( netlifyRedirectUrl !== null ) {
return Astro.redirect( netlifyRedirectUrl.to )
}
return null
}

View file

@ -1,6 +1,3 @@
// import { promises as fs } from 'fs'
import fs from 'fs-extra'
import MarkdownIt from 'markdown-it'
import axios from 'axios'
@ -12,7 +9,11 @@ import parseDate from './parse-date'
import { eitherMatches } from './matching.js'
import { getAppEndpoint } from './app-derived'
import { makeSlug } from './slug.js'
import { byTimeThenNull } from './sort-list.js'
import {
cliOptions
} from '~/helpers/cli-options.js'
const md = new MarkdownIt()
@ -156,7 +157,9 @@ export function buildReadmeAppList ({ readmeContent, scanListMap, commits }) {
} )
}
console.log(`Merged ${alias} (${scannedApp.bundleIds[0]}) from scanned apps into ${name} from README`)
if ( cliOptions.showMerges || cliOptions.verbose ) {
console.log(`Merged ${alias} (${scannedApp.bundleIds[0]}) from scanned apps into ${name} from README`)
}
scanListMap.delete( key )
}
}
@ -435,16 +438,8 @@ export default async function () {
// console.log('appList', appList)
return [
return ([
...appList,
...Array.from( scanListMap, ([name, value]) => value )
]
// fs.readFile('../README.md', 'utf8')
// .then((err, data) => {
// const result = md.parse(data)
// console.log('result', result)
// return result
// })
]).sort( byTimeThenNull )
}

View file

@ -1,5 +1,4 @@
import axios from 'axios'
import fs from 'fs-extra'
import { PromisePool } from '@supercharge/promise-pool'
import { fuzzyMatchesWholeWord } from './matching.js'
@ -7,6 +6,7 @@ import { byTimeThenNull } from './sort-list.js'
import { getVideoEndpoint } from './app-derived.js'
import parseDate from './parse-date'
import { makeSlug } from './slug.js'
import { youtubeVideoPath } from '~/helpers/api/youtube/build.js'
const inTimestamps = ( name, video ) => {
@ -140,13 +140,16 @@ async function handleFetchedVideo ( fetchedVideo, videoId, applist ) {
// Build video slug
const slug = makeSlug( `${fetchedVideo.title}-i-${videoId}` )
const apps = []
const appLinks = []
// Generate new tag set based on api data
const tags = generateVideoTags(fetchedVideo)
for ( const app of applist ) {
if (videoFeaturesApp(app, fetchedVideo)) {
apps.push(app.slug)
appLinks.push({
name: app.name,
endpoint: app.endpoint
})
tags.add(app.category.slug)
}
@ -165,7 +168,7 @@ async function handleFetchedVideo ( fetchedVideo, videoId, applist ) {
name: fetchedVideo.title,
id: videoId,
lastUpdated,
apps,
appLinks,
slug,
channel: {
name: fetchedVideo.rawData.snippet.channelTitle,
@ -184,12 +187,12 @@ async function handleFetchedVideo ( fetchedVideo, videoId, applist ) {
export default async function ( applist ) {
const videosJsonUrl = process.env.VIDEO_SOURCE || `${process.env.VFUNCTIONS_URL}/videos.json`
// const videosJsonUrl = process.env.VIDEO_SOURCE || `${process.env.VFUNCTIONS_URL}/videos.json`
// Fetch Commits
const response = await axios.get( videosJsonUrl )
// const response = await axios.get( videosJsonUrl )
// Extract commit from response data
const fetchedVideos = response.data
const fetchedVideos = await fs.readJson( youtubeVideoPath )//response.data
const videos = []

13
helpers/bundles.js Normal file
View file

@ -0,0 +1,13 @@
export function supportedArchitectures ( appScan ) {
// if ( Array.isArray(appScan['Macho Meta']) ) {
// return appScan['Macho Meta'].map( architecture => architecture.processorType)
// }
// console.log('meta', appScan['Macho Meta'])
if ( appScan['Macho Meta'].architectures === undefined ) return []
return appScan['Macho Meta'].architectures
.map( architecture => architecture.processorType)
.filter(processorType => Number(processorType) !== 0)
}

View file

@ -1,10 +1,14 @@
// Universal JS imports only
import shuffle from 'just-shuffle'
import { makeSlug } from './slug.js'
export function makeCategorySlug ( categoryName ) {
return makeSlug( categoryName )
}
// Maps iTunes Genres titles to Category IDs
const categoryMap = new Map([
[ 'Business', 2 ],
[ 'Entertainment', 5 ],
@ -18,7 +22,12 @@ const categoryMap = new Map([
// [ 'Name', 1 ],
// Needs work before apps can be assigned games category
// so for now 'Games' genre is Entertainment
// Games will be assigned to the "Games" category
// but need to be put into games kind/list
// so the solution may be to separate games from apps
// during app list build and then merge "Game from Apps"
// into the "Games" list
// but for now 'Games' genre is Entertainment
// [ 'Games', 100 ],
[ 'Games', 5 ],
])
@ -42,7 +51,8 @@ export const categoryTemplate = {
pluralLabel: null,
itemSuffixLabel: null,
icon: null,
requestLinks: null
requestLinks: null,
icon: '💻',
}
export const categories = {
@ -59,6 +69,7 @@ export const categories = {
label: 'Developer Tools',
pluralLabel: 'Developer Tools',
slug: 'developer-tools',
snakeSlug: 'developer_tools',
},
'productivity-tools': {
@ -67,6 +78,7 @@ export const categories = {
label: 'Productivity Tools',
pluralLabel: 'Productivity Tools',
slug: 'productivity-tools',
snakeSlug: 'productivity_tools',
},
'video-and-motion-tools': {
@ -75,6 +87,7 @@ export const categories = {
label: 'Video and Motion Tools',
pluralLabel: 'Video and Motion Tools',
slug: 'video-and-motion-tools',
snakeSlug: 'video_and_motion_tools',
},
'social-and-communication': {
@ -83,6 +96,7 @@ export const categories = {
label: 'Social and Communication',
pluralLabel: 'Social and Communication Apps',
slug: 'social-and-communication',
snakeSlug: 'social_and_communication',
},
'entertainment-and-media-apps': {
@ -91,6 +105,7 @@ export const categories = {
label: 'Entertainment and Media Apps',
pluralLabel: 'Entertainment and Media Apps',
slug: 'entertainment-and-media-apps',
snakeSlug: 'entertainment_and_media_apps',
},
'music-and-audio-tools': {
@ -99,6 +114,7 @@ export const categories = {
label: 'Music and Audio Tools',
pluralLabel: 'Music and Audio Tools',
slug: 'music-and-audio-tools',
snakeSlug: 'music_and_audio_tools',
},
'photo-and-graphic-tools': {
@ -107,6 +123,7 @@ export const categories = {
label: 'Photo and Graphic Tools',
pluralLabel: 'Photo and Graphic Tools',
slug: 'photo-and-graphic-tools',
snakeSlug: 'photo_and_graphic_tools',
},
'science-and-research-software': {
@ -115,6 +132,7 @@ export const categories = {
label: 'Science and Research Software',
pluralLabel: 'Science and Research Software',
slug: 'science-and-research-software',
snakeSlug: 'science_and_research_software',
},
'3d-and-architecture': {
@ -123,6 +141,7 @@ export const categories = {
label: '3D and Architecture',
pluralLabel: '3D and Architecture Applications',
slug: '3d-and-architecture',
snakeSlug: '3d_and_architecture',
},
'vpns-security-and-privacy': {
@ -131,6 +150,7 @@ export const categories = {
label: 'VPNs, Security, and Privacy',
pluralLabel: 'VPN, Security, and Privacy Applications',
slug: 'vpns-security-and-privacy',
snakeSlug: 'vpns_security_and_privacy',
},
'live-production-and-performance': {
@ -139,6 +159,7 @@ export const categories = {
label: 'Live Production and Performance',
pluralLabel: 'Live Production and Performance Software',
slug: 'live-production-and-performance',
snakeSlug: 'live_production_and_performance',
},
'system-tools': {
@ -147,6 +168,7 @@ export const categories = {
label: 'System Tools',
pluralLabel: 'System Tools',
slug: 'system-tools',
snakeSlug: 'system_tools',
},
// Special Lists
@ -156,6 +178,7 @@ export const categories = {
label: 'Games',
pluralLabel: 'Games',
slug: 'games',
snakeSlug: 'games',
icon: '🎮',
requestLinks: [
{
@ -171,6 +194,7 @@ export const categories = {
pluralLabel: 'Homebrew Formulae',
itemSuffixLabel: 'via Homebrew',
slug: 'homebrew',
snakeSlug: 'homebrew',
icon: '🍺'
},
@ -182,9 +206,25 @@ export const categories = {
label: 'Uncategorized',
pluralLabel: 'Uncategorized Software',
slug: 'uncategorized',
snakeSlug: 'uncategorized',
},
}
// Maps categories to kinds and vice versa
const categoryToKind = {
...Object.fromEntries( Object.keys( categories ).map( key => [ key, key ] ) ),
'homebrew': 'formula',
'games': 'game',
}
// Respective directory for each category
export function getCategoryKindName ( categorySlug ) {
return categoryToKind[ categorySlug ]
}
export function getKindToCategorySlug ( kindSlug ) {
return Object.keys( categories ).find( key => categoryToKind[ key ] === kindSlug )
}
export const categoriesById = Object.fromEntries( Object.entries( categories ).map( ([ key, category ]) => [category.id, { ...category, key } ] ) )
@ -211,6 +251,16 @@ export function getAppCategory (app) {
return categories[app.category.slug]
}
const categoryFilterPrefix = 'category_'
export function makeCategoryFilterFromListing ( listing ) {
return `${ categoryFilterPrefix }${ getAppCategory( listing ).snakeSlug }`
}
export function makeCategoryFilterFromCategorySlug ( categorySlug ) {
const category = categories[ categorySlug ]
return `${ categoryFilterPrefix }${ category.snakeSlug }`
}
export function findCategoryForTagsSet ( tags ) {
@ -230,3 +280,20 @@ export function findCategoryForTagsSet ( tags ) {
return null
}
const sampleListFormatter = new Intl.ListFormat( 'en', { style: 'long', type: 'unit' })
export function makeSummaryOfListings ({
list,
length = 25,
random = false,
suffix = ', etc...',
} = {}) {
const listToSample = random ? shuffle( list ) : list
const samples = listToSample
.slice(0, length)
.map( listing => listing.name )
return sampleListFormatter.format( samples ) + suffix
}

View file

@ -4,6 +4,25 @@ export function isString( maybeString ) {
return (typeof maybeString === 'string' || maybeString instanceof String)
}
export function isNonEmptyString ( maybeString ) {
if ( !isString( maybeString ) ) return false
return maybeString.length > 0
}
export function isNonEmptyArray ( maybeArray ) {
if ( !Array.isArray( maybeArray ) ) return false
return maybeArray.length > 0
}
export function isPositiveNumberString ( maybeNumber ) {
if ( !isString( maybeNumber ) ) return false
return /\d+$/.test( maybeNumber )
}
export function isValidHttpUrl( maybeUrl, allowUnsecure = false ) {
if ( !isString( maybeUrl ) ) return false
@ -22,6 +41,17 @@ export function isValidHttpUrl( maybeUrl, allowUnsecure = false ) {
return url.protocol === "https:"
}
export function isValidImageUrl ( maybeUrl ) {
if ( !isValidHttpUrl( maybeUrl ) ) return false
// Check if url has a file extension
const url = new URL(maybeUrl)
const fileExtension = url.pathname.split('.').pop()
return isNonEmptyString( fileExtension )
}
export function isObject( maybeObject ) {
return maybeObject === Object( maybeObject )
}

8
helpers/cli-options.js Normal file
View file

@ -0,0 +1,8 @@
export const commandArguments = process.argv
export const cliOptions = {
verbose: commandArguments.includes('--verbose'),
withApi: commandArguments.includes('--with-api'),
noLists: commandArguments.includes('--no-lists'),
showMerges: commandArguments.includes('--show-merges'),
}

323
helpers/config-node.js Normal file
View file

@ -0,0 +1,323 @@
import TOML from '@iarna/toml'
import fs from 'fs-extra'
import pkg from '~/package.json'
import { getSiteUrl } from '~/helpers/url.js'
export const siteUrl = getSiteUrl()
export const nuxtHead = {
// this htmlAttrs you need
htmlAttrs: {
lang: 'en',
},
title: 'Does It ARM',
description: pkg.description,
meta: [
{ charset: 'utf-8' },
{
name: 'viewport',
content: 'width=device-width, initial-scale=1'
},
{
hid: 'description',
name: 'description',
content: pkg.description
},
{
'property': 'og:image',
'content': `${ siteUrl }/images/og-image.png`
},
{
'property': 'og:image:width',
'content': '1200'
},
{
'property': 'og:image:height',
'content': '627'
},
{
'property': 'og:image:alt',
'content': 'Does It ARM Logo'
},
// Twitter Card
{
'property': 'twitter:card',
'content': 'summary'
},
{
'property': 'twitter:title',
'content': 'Does It ARM'
},
{
'property': 'twitter:description',
'content': pkg.description
},
{
'property': 'twitter:url',
'content': `${ siteUrl }`
},
{
'property': 'twitter:image',
'content': `${ siteUrl }/images/mark.png`
}
],
link: [
// Favicon
{
rel: 'icon',
type: 'image/x-icon',
href: '/favicon.ico'
},
// Gtag Preconnect
{
rel: 'preconnect',
href: 'https://www.googletagmanager.com'
},
// Carbon Preconnects
{
rel: 'preconnect',
href: 'https://cdn.carbonads.com'
},
{
rel: 'preconnect',
href: 'https://srv.carbonads.net'
},
{
rel: 'preconnect',
href: 'https://cdn4.buysellads.net'
},
],
script: [
// // Carbon Ads
// // https://sell.buysellads.com/zones/1294/ad-tags#z=js
// {
// // <script async type="text/javascript" src="//cdn.carbonads.com/carbon.js?serve=CK7DVK3M&placement=doesitarmcom" id="_carbonads_js"></script>
// src: 'https://cdn.carbonads.com/carbon.js?serve=CK7DVK3M&placement=doesitarmcom',
// async: true,
// type: 'text/javascript',
// id: '_carbonads_js',
// class: 'include-on-static carbon-inline-wide',
// body: true
// }
]
}
export async function getNetlifyConfig () {
const configPath = './netlify.toml'
const tomlContent = await fs.readFile(configPath, 'utf-8')
const netlifyConfig = TOML.parse(tomlContent)
// console.log('netlifyConfig', netlifyConfig)
// console.log('tomlContent', tomlContent)
return netlifyConfig
}
export async function getNetlifyRedirect ( path ) {
// Check if the path is valid
// by checking if it starts with a slash
// and does not end with a slash
// if ( !path.startsWith('/') || path.endsWith('/') ) {
// throw new Error(`Invalid Netlify redirect path: ${ path }`)
// }
const netlifyConfig = await getNetlifyConfig()
const redirects = netlifyConfig.redirects
for ( const redirect of redirects ) {
// Check if the from path is valid
// by checking if it starts with a slash
// and does not end with a slash
if ( !redirect.from.startsWith('/') || redirect.from.endsWith('/') ) {
throw new Error(`Invalid Netlify redirect.from path: ${ redirect.from }`)
}
if ( redirect.from === path ) {
return redirect
}
}
return null
}
export function makeTitle ( listing ) {
return `Does ${ listing.name } work on Apple Silicon? - ${ nuxtHead.title }`
}
export function makeDescription ( listing ) {
return `Latest reported support status of ${ listing.name } on Apple Silicon and ${ this.$config.processorsVerbiage } Processors.`
}
function makeTag ( tag, tagName = 'meta' ) {
const attributes = Object.entries(tag).map( ([ name, value ]) => `${name}="${value}"` ).join(' ')
return `<${tagName} ${attributes}>`
}
function mapMetaTag ( tag ) {
if ( tag.hasOwnProperty('property') ) {
return [
`property-${tag.property}`,
makeTag(tag)
]
}
if ( tag.hasOwnProperty('name') ) {
return [
`name-${tag.name}`,
makeTag(tag)
]
}
if ( tag.hasOwnProperty('charset') ) {
return [
'charset',
makeTag(tag)
]
}
}
function mapLinkTag ( tag ) {
return [
`type-${tag.type}`,
makeTag(tag, 'link')
]
}
export class PageHead {
constructor ( options = {} ) {
const {
title,
description,
meta = [],
link = [],
structuredData = null,
domain = getSiteUrl(),
pathname,
} = options
this.title = title
this.description = description
this.meta = meta
this.link = link
this.structuredData = structuredData
this.domain = domain
this.pathname = pathname
}
get pageUrl () {
const urlInstance = new URL( this.domain )
urlInstance.pathname = this.pathname
return urlInstance
}
get pageUrlString () {
return this.pageUrl.toString()
}
get defaultMeta () {
return nuxtHead.meta
}
get defaultMetaTags () {
return Object.fromEntries( nuxtHead.meta.map( mapMetaTag ) )
}
get defaultLinkTags () {
return Object.fromEntries( nuxtHead.link.map( mapLinkTag ) )
}
get pageMeta () {
// console.log('this.defaultMeta', this.defaultMeta)
return [
...this.defaultMeta,
{
'property': 'twitter:url',
'content': this.pageUrlString
},
...this.meta
]
}
get metaTags () {
const metaTags = {
// ...this.defaultMeta,
// 'property-twitter:url': `<meta property="twitter:url" content="${ this.pageUrlString }">`,
...Object.fromEntries( this.pageMeta.map(mapMetaTag) )
}
// Get description from data
if ( this.description ) {
// Set meta description
metaTags['name-description'] = `<meta hid="description" name="description" content="${ this.description }">`
// Set twitter description
metaTags['property-twitter:description'] = `<meta hid="twitter:description" property="twitter:description" content="${ this.description }">`
}
// Get title from data
if ( this.title ) {
// Set twitter title
metaTags['property-twitter:title'] = `<meta hid="twitter:title" property="twitter:title" content="${ this.title }">`
}
return metaTags
}
get metaMarkup () {
return Object.values( this.metaTags ).join('')
}
get linkTags () {
const linkTags = {
...this.defaultLinkTags,
...Object.fromEntries( this.link.map( mapLinkTag ) )
}
return linkTags
}
get linkMarkup () {
return Object.values( this.linkTags ).join('')
}
get metaAndLinkMarkup () {
return [
this.metaMarkup,
this.linkMarkup
].join('')
}
get structuredDataMarkup () {
if ( structuredData === null ) return ''
const structuredDataJson = JSON.stringify( structuredData )
return `<script type="application/ld+json">${ structuredDataJson }</script>`
}
}

9
helpers/constants.js Normal file
View file

@ -0,0 +1,9 @@
export const filterSeparator = '_'
export const sitemapLocation = './static/'
export const sitemapIndexFileName = 'sitemap-index.xml'
// https://analytics.google.com/analytics/web/#/a28434239p302384837/admin/streams/table/3228170828
export const gaMeasurementId = 'G-0WLH5YTTB0'

15
helpers/environment.js Normal file
View file

@ -0,0 +1,15 @@
import has from 'just-has'
export function isNuxt( VueThis ) {
return has( VueThis, [ '$nuxt' ])
}
// https://stackoverflow.com/a/8684009/1397641
export function isDarwin () {
if ( typeof navigator !== 'undefined' ) return false
if ( typeof process === 'undefined' ) return false
return process.platform === 'darwin'
}

View file

@ -1,6 +1,9 @@
import statuses from '~/helpers/statuses'
export default function ( appList ) {
if ( !Array.isArray( appList ) ) {
throw new Error(`List must be an array but is ${typeof appList}`)
}
const totalApps = appList.length

214
helpers/listing-page.js Normal file
View file

@ -0,0 +1,214 @@
import has from 'just-has'
// https://anguscroll.com/just/just-replace-all
import replaceAll from 'just-replace-all'
import {
getAppType
} from './app-derived.js'
import { buildVideoStructuredData } from './structured-data.js'
import { nuxtHead } from '~/helpers/config-node.js'
import {
getPartPartsFromUrl
} from './url.js'
export const samChannelId = 'UCB3jOb5QVjX7lYecvyCoTqQ'
function makeTitle ( listing ) {
return `Does ${ listing.name } work on Apple Silicon? - ${ nuxtHead.title }`
}
function makeDescription ( listing ) {
// const processorsVerbiage = process.env.npm_package_config_verbiage_processors || this.$config.processorsVerbiage
const processorsVerbiage = 'Apple M2 and M1 Ultra'
return `Latest reported support status of ${ listing.name } on Apple Silicon and ${ processorsVerbiage } Processors.`
}
function convertYoutubeImageUrl ( stringWithUrls, extension ) {
let workingString = stringWithUrls
workingString = replaceAll( stringWithUrls, 'ytimg.com/vi/', `ytimg.com/vi_${ extension }/`)
workingString = workingString.replace(/.png|.jpg|.jpeg/g, `.${ extension }`)
return workingString
}
export function getVideoImages ( video ) {
// Catch the case where the video has no thumbnails
if ( !has( video, 'thumbnail' ) ) throw new Error('No thumbnail found')
const webpSource = {
...video.thumbnail,
srcset: convertYoutubeImageUrl( video.thumbnail.srcset, 'webp' ),
src: convertYoutubeImageUrl( video.thumbnail.src, 'webp' ),
type: 'image/webp'
}
const jpgSource = {
...video.thumbnail,
type: 'image/jpeg'
}
const sources = {
webp: webpSource,
jpeg: jpgSource
}
// Responsive Preloads - https://web.dev/preload-responsive-images/
// Responsive Preloads with image types - https://blog.laurenashpole.com/post/658079409151016960/preloading-images-in-a-responsive-webp-world
// <link rel="preload" as="image" href="large-image.webp" media="(min-width: 768px)" imagesrcset="large-image.webp, large-image-2x.webp 2x" type="image/webp" />
const preloads = Object.entries( sources ).map( ([ typeKey, typeSource ]) => {
return {
'rel': 'preload',
'as': 'image',
'href': typeSource.src,
'media': typeSource.sizes,
'imagesrcset': typeSource.srcset,
'type': typeSource.type
}
})
return {
imgSrc: video.thumbnail.src,
sources,
preloads
}
}
export function makeApiPathFromEndpoint ( endpoint ) {
const [
kind,
listingSlug
] = getPartPartsFromUrl( endpoint )
return `/api/${ kind }/${ listingSlug }.json`
}
export class ListingDetails {
constructor ( listing ) {
this.api = listing
this.type = getAppType( listing )
}
type = ''
get isGame () {
return this.type === 'game'
}
isListingDetails = true
get mainHeading () {
// Use the video title for videos
if ( this.type === 'video' ) {
return this.api.name
}
if ( this.type === 'formula' ) {
return `Does <code class="bg-darkest rounded px-2 py-1">${ this.api.name }</code> work on Apple Silicon when installed via Homebrew?`
}
return `Does ${ this.api.name } work on Apple Silicon?`
}
get subtitle () {
return this.api.text
}
get pageTitle () {
return makeTitle( this.api )
}
get pageDescription () {
return makeDescription( this.api )
}
get endpointParts () {
return getPartPartsFromUrl( this.api.endpoint )
}
get apiEndpointPath () {
return makeApiPathFromEndpoint( this.api.endpoint )
}
get relatedVideos () {
if ( Array.isArray( this.api.relatedVideos ) ) {
return this.api.relatedVideos
}
if ( !!this.api.payload && Array.isArray( this.api.payload.relatedVideos ) ) {
return this.api.payload.relatedVideos
}
return []
}
get hasRelatedVideos () {
return this.relatedVideos.length > 0
}
get hasRelatedApps () {
return Array.isArray( this.api.appLinks ) && this.api.appLinks.length > 0
}
get hasBenchmarksPage () {
return this.hasRelatedVideos
}
get shouldHaveSubscribeButton () {
if ( this.initialVideo === null ) return false
return this.initialVideo.channel.id !== samChannelId
}
get initialVideo () {
if ( this.type === 'video' ) {
return this.api
}
if ( this.hasRelatedVideos ) {
return this.api.relatedVideos[0]
}
return null
}
get hasInitialVideo () {
return this.initialVideo !== null
}
get structuredData () {
if ( this.type === 'video' ) {
return buildVideoStructuredData( this.api, this.api.appLinks )
}
return null
}
get headOptions () {
return {
title: this.pageTitle,
description: this.pageDescription,
// meta,
link: [],
structuredData: this.structuredData,
// domain,
pathname: this.api.endpoint,
}
}
}
export function ensureListingDetails ( listing ) {
if ( listing.isListingDetails ) {
return listing
}
return new ListingDetails( listing )
}

View file

@ -1,6 +1,6 @@
// https://github.com/paulirish/lite-youtube-embed/blob/master/src/lite-yt-embed.js
// import canAutoPlay from 'can-autoplay'
import canAutoPlay from 'can-autoplay'
/**
@ -47,10 +47,10 @@ class LiteYTEmbed extends HTMLElement {
this.playerContainer = this.querySelector('.player-container')
this.playerPoster = this.querySelector('.player-poster')
// console.log('canAutoplay from connectedCallback', canAutoplay)
// console.log('canAutoPlay from connectedCallback', canAutoPlay)
console.log('video', this.video)
console.log('this.playerContainer', this.playerContainer)
// console.log('video', this.video)
// console.log('this.playerContainer', this.playerContainer)
// Start watchers here
@ -83,7 +83,7 @@ class LiteYTEmbed extends HTMLElement {
this.detectAutoplay()
.then( ({ willAutoplay }) => {
console.log('willAutoplay', willAutoplay)
// console.log('willAutoplay', willAutoplay)
// If we're allowed to autoplay
// then start loading the player
@ -286,7 +286,7 @@ class LiteYTEmbed extends HTMLElement {
// const { default: canAutoPlay } = await import('can-autoplay')
const willAutoplay = await canAutoplay.video()
const willAutoplay = await canAutoPlay.video()
// const willAutoplayMuted = await canAutoPlay.video({ muted: true, inline: true })
return {

View file

@ -119,7 +119,7 @@ export function MachoParser(file, callback) {
var cmd = null;
if(type == LOAD_COMMAND_TYPE.LC_SEGMENT) {
if(data.length < 48) {
if(process.env.VERBOSE){ console.log('Segment command OOB'); }
// if(process.env.VERBOSE){ console.log('Segment command OOB'); }
return new LoadCommand(type, data, size, off);
}
let name = new Cstr(data.slice(0, (4*uint32_t)));
@ -173,7 +173,7 @@ export function MachoParser(file, callback) {
let data = new Uint8Array(e.target.result);
let magics = FindMagic(data, false); //Try to find all Mach-O magics in the byte array
if ( process.env.DEBUG ) { console.log('Parsing all magics...'); }
// if ( process.env.DEBUG ) { console.log('Parsing all magics...'); }
//If magics where found, parse the binary.
if(magics.length > 0) {

View file

@ -0,0 +1,25 @@
import dotenv from 'dotenv'
dotenv.config()
export const publicRuntimeConfig = {
allUpdateSubscribe: process.env.ALL_UPDATE_SUBSCRIBE,
testResultStore: process.env.TEST_RESULT_STORE,
siteUrl: process.env.URL,
macsVerbiage: process.env.npm_package_config_verbiage_macs,
processorsVerbiage: process.env.npm_package_config_verbiage_processors,
}
export function makeViteDefinitions () {
const definitions = {}
for ( const key in publicRuntimeConfig ) {
definitions[`this.$config.${key}`] = JSON.stringify( publicRuntimeConfig[key] )
definitions[`global.$config.${key}`] = JSON.stringify( publicRuntimeConfig[key] )
definitions[`global.$config`] = JSON.stringify( publicRuntimeConfig )
}
return definitions
}

View file

@ -1,8 +1,19 @@
// import { allVideoAppsListSet } from '~/helpers/get-list.js'
// import videoList from '~/static/video-list.json'
import { getAppEndpoint, getAppType } from '~/helpers/app-derived.js'
function videoHasAppEndpoint ( video, appEndpoint ) {
for (const appLink of video.appLinks) {
if ( appLink.endpoint === appEndpoint ) {
return true
}
}
return false
}
export function appsRelatedToVideo ( video, allVideoAppsListSet ) {
// console.log('allVideoAppsListSet', allVideoAppsListSet.length)
const relatedApps = []
@ -10,7 +21,7 @@ export function appsRelatedToVideo ( video, allVideoAppsListSet ) {
for (const app of allVideoAppsListSet) {
// console.log('video', video)
// Skip this app if it's not listed in the videos apps
if (!video.apps.includes(app.slug)) continue
if (!videoHasAppEndpoint( video, app.endpoint )) continue
// Add this app to our featured app list
relatedApps.push(app)
@ -22,9 +33,6 @@ export function appsRelatedToVideo ( video, allVideoAppsListSet ) {
export function videosRelatedToVideo ( video, allVideoAppsListSet, videoListSet ) {
const relatedVideos = {}
// console.log('videoList', videoList[0])
// console.log('allVideoAppsListSet', allVideoAppsListSet[0])
const featuredApps = appsRelatedToVideo( video, allVideoAppsListSet )
// Find other videos that also feature this video's app
@ -32,7 +40,7 @@ export function videosRelatedToVideo ( video, allVideoAppsListSet, videoListSet
for (const app of featuredApps) {
// console.log('otherVideo', otherVideo)
// Skip if this app is not in the other video's apps
if (!otherVideo.apps.includes(app.slug)) continue
if (!videoHasAppEndpoint( otherVideo, app.endpoint )) continue
// Skip if the other video is, in fact, this video
if (otherVideo.slug === video.slug) continue
@ -50,14 +58,38 @@ export function videosRelatedToApp ( app, videoListSet ) {
// console.log('videoListSet', videoListSet)
const relatedVideos = {}
const relatedVideos = []
// Find other videos that also feature this video's app
for (const video of videoListSet) {
if (!video.apps.includes(app.slug)) continue
relatedVideos[video.id] = video
if (!videoHasAppEndpoint( video, app.endpoint )) continue
relatedVideos.push( video )
if ( relatedVideos.length > 20 ) break
}
return Object.values(relatedVideos)
return relatedVideos
}
export function videoBenchmarksRelatedToApp ( app, videoListSet ) {
return videosRelatedToApp( app, videoListSet ).map(video => {
return {
...video,
endpoint: `${getAppEndpoint( app )}/benchmarks#${video.id}`
}
})
}
export function getRelatedVideos ( { listing, videoListSet, appListSet } = {} ) {
const listingType = getAppType( listing )
if ( listingType === 'video' ) {
return videosRelatedToVideo( listing, appListSet, videoListSet )
}
return videoBenchmarksRelatedToApp( listing, videoListSet )
}

View file

@ -1,6 +1,6 @@
function scrollHorizontalCarousel ( event ) {
export function scrollHorizontalCarousel ( event ) {
event.stopPropagation()
// console.log('event.target', event.currentTarget)

View file

@ -1,17 +1,65 @@
import { filterSeparator } from '~/helpers/constants.js'
const statuses = {
'✅': 'native',
'✳️': 'rosetta',
'⏹': 'no-in-progress',
'🚫': 'no',
'🔶': 'unreported',
'native': {
icon: '✅',
filterLabel: 'Native Support',
snakeSlug: 'native',
},
'rosetta': {
icon: '✳️',
filterLabel: 'Rosetta',
snakeSlug: 'rosetta',
},
'no-in-progress': {
icon: '⏹',
filterLabel: 'In Progress',
snakeSlug: 'no_in_progress',
},
'no': {
icon: '🚫',
filterLabel: 'Unsupported',
snakeSlug: 'no',
},
'unreported': {
icon: '🔶',
filterLabel: 'Unreported',
snakeSlug: 'unreported',
}
}
const statusesByIcon = Object.keys( statuses ).reduce( ( acc, key ) => {
const status = statuses[ key ]
acc[ status.icon ] = key
return acc
}, {} )
export const statusFilterPrefix = 'status'
// Example:
// {
// label: '✅ Native Support',
// query: 'status_native'
// },
export const defaultStatusFilters = Object.keys( statuses ).reduce( ( acc, key ) => {
const status = statuses[ key ]
acc[ statusFilterPrefix + filterSeparator + key ] = status.filterLabel
acc = [...acc, {
label: `${ status.icon } ${ status.filterLabel }`,
query: statusFilterPrefix + filterSeparator + status.snakeSlug
}]
return acc
}, [] )
export function getStatusName ( status ) {
for (const key in statuses) {
if (status.trim().startsWith( key )) return statuses[key]
for (const key in statusesByIcon) {
if (status.trim().startsWith( key )) return statusesByIcon[key]
}
throw new Error('Non status matched')
@ -33,4 +81,4 @@ export function getStatusOfScan ( appScan, includeVersion = true ) {
}
export default statuses
export default statusesByIcon

396
helpers/stork/browser.js Normal file
View file

@ -0,0 +1,396 @@
import { filterSeparator } from '~/helpers/constants.js'
import { isString } from '~/helpers/check-types.js'
import {
storkIndexRelativeURL,
storkScriptURL
} from '~/helpers/stork/config.js'
export function makeHighlightedMarkup ( options = {} ) {
const {
text,
highlight_ranges,
withElipsis = true,
} = options
if ( highlight_ranges.length === 0 ) return [ text ]
const highlighted_text = highlight_ranges.map( range => {
const {
beginning,
end
} = range
const before = text.slice( 0, beginning )
const target = text.slice( beginning, end + 1 )
const after = text.slice( end + 1 )
// console.log({
// before,
// target,
// after
// })
// `<span class="stork-highlighted-text">${ highlighted_text }</span>`
return [
withElipsis ? '...' : '',
before.trim(),
/* html */` <span class="stork-highlighted-text font-bold text-white bg-green-800 rounded px-1">${ target.trim() }</span> `,
after.trim(),
withElipsis ? '...' : '',
].join('')
} )
return highlighted_text
}
export function makeHighlightedResultTitle ( result ) {
const [ highlightedTitleMarkup ] = makeHighlightedMarkup({
text: result.entry.title,
highlight_ranges: result.title_highlight_ranges,
withElipsis: false
})
// console.log('highlightedTitleMarkup', highlightedTitleMarkup)
// console.log('result', result)
if ( !isString( highlightedTitleMarkup ) ) throw new Error('highlightedTitleMarkup is not a string')
return highlightedTitleMarkup
}
export class StorkClient {
constructor ( options = {} ) {
this.name = options.name || 'index'
this.url = options.url || storkIndexRelativeURL
// Configuration Reference - https://stork-search.net/docs/js-ref#showProgress
// Example - https://github.com/jameslittle230/stork/blob/ff49f163db06734e18ab690c188b45a3c68442ae/js/config.ts#L4
this.config = {
minimumQueryLength: 1,
showScores: true,
...options.config || {}
}
// Stork instance
this.stork = options.stork || null
this.cancelCurrentQuery = null
}
setupState = 'not-setup'
get isSetup () {
return this.setupState === 'complete'
}
resultHasTerm ( result, term ) {
const {
entry: {
url,
title
},
excerpts
} = result
if ( title.toLowerCase().includes( term.toLowerCase() ) ) return true
if ( url.toLowerCase().includes( term.toLowerCase() ) ) return true
for ( const excerpt of excerpts ) {
if ( excerpt.text.toLowerCase().includes( term.toLowerCase() ) ) return true
}
return false
}
resultHasAnyTerm ( result, terms ) {
for ( const term of terms ) {
if ( this.resultHasTerm( result, term ) ) return true
}
return false
}
resultHasAllTerms ( result, terms ) {
for ( const term of terms ) {
if ( !this.resultHasTerm( result, term ) ) return false
}
return true
}
search ( query ) {
if ( !this.isSetup ) throw new Error('Not setup')
// search() - https://github.com/jameslittle230/stork/blob/ff49f163db06734e18ab690c188b45a3c68442ae/js/main.ts#L55
return this.stork.search( this.name, query )
}
// Loads the Stork WASM and Index into the browser on first query
// so that we don't have to load them initially.
async lazyQuery ( query, requiredTerms = [] ) {
// Sleep
// await new Promise( resolve => setTimeout( resolve, 50000000 ) )
const result = await new Promise( async ( resolve, reject ) => {
// If there an existing query to cancel
// then cancel it
// so that we don't race bad conditions
// such as earrly queries beating the final one
if ( this.cancelCurrentQuery !== null ) {
this.cancelCurrentQuery()
}
// Plugin this promise to our cancel method
this.cancelCurrentQuery = () => { reject({ message: `Cancelled previous query for ${ query }`, canceled: true }) }
if ( !this.isSetup ) await this.setup()
// console.log('debounce', this.query)
const queryResponse = this.search( query )
if ( requiredTerms.length !== 0 ) {
// Filter out results that don't have the required terms
const filteredResults = queryResponse.results.filter( result => {
return this.resultHasAllTerms( result, requiredTerms )
})
resolve( {
...queryResponse,
results: filteredResults
} )
return
}
resolve( queryResponse )
}).catch( err => {
console.log('Query rejected', err)
return null
})
console.log( 'result', result )
return result
}
waitForSetup () {
return new Promise( resolve => {
if ( this.isSetup ) resolve()
// Start timer to check for setup
const timer = setInterval( () => {
if ( this.isSetup ) {
clearInterval( timer )
resolve()
}
}, 50 )
})
}
loadStorkScript () {
return new Promise((resolve, reject) => {
if ( !!this.stork ) resolve()
if ( !!window.stork ) {
this.stork = window.stork
resolve()
}
const s = document.createElement('script')
let r = false
s.type = 'text/javascript'
s.src = storkScriptURL
s.async = true
s.onerror = function(err) {
reject(err, s)
}
s.onload = s.onreadystatechange = () => {
// console.log(this.readyState); // uncomment this line to see which ready states are called.
if (!r && (!this.readyState || this.readyState == 'complete')) {
r = true
this.stork = window.stork
// console.log('window.stork', typeof window.stork)
resolve()
}
}
const t = document.getElementsByTagName('script')[0]
t.parentElement.insertBefore(s, t)
})
}
// https://github.com/jameslittle230/stork/blob/ff49f163db06734e18ab690c188b45a3c68442ae/js/main.ts#L40
async setup () {
// Prevent multiple setups
if ( this.setupState !== 'not-setup' ) {
await this.waitForSetup()
return
}
// We're the first to setup
// so let's set the state to prevent duplicates
this.setupState = 'pending'
// Load Stork Script
if ( !this.stork ) {
// console.log('Loading stork script...')
await this.loadStorkScript()
}
const {
initialize,
downloadIndex,
} = this.stork
// Stork JavaScript Reference - https://stork-search.net/docs/js-ref
// initialize() - https://github.com/jameslittle230/stork/blob/ff49f163db06734e18ab690c188b45a3c68442ae/js/main.ts#L14
const initPromise = initialize()
// downloadIndex() - https://github.com/jameslittle230/stork/blob/ff49f163db06734e18ab690c188b45a3c68442ae/js/main.ts#L20
const downloadPromise = downloadIndex( this.name, this.url, this.config )
// This silly `then` call turns a [(void), (void)] into a (void), which is
// only necessary to make Typescript happy.
// You begin to wonder if you write Typescript code, or if Typescript code writes you.
await Promise.all([ initPromise, downloadPromise ])
// Mark setup as complete
this.setupState = 'complete'
return
}
}
export class StorkFilters {
constructor({
initialFilters = {}
} = {}) {
this.initialFilters = initialFilters
this.filters = {
...initialFilters
}
}
get list () {
return Object.entries( this.filters ).map( ([ filterKey, filterValue ]) => {
return `${ filterKey }${ filterSeparator }${ filterValue }`
} )
}
get asQuery () {
return this.list.join(' ')
}
getByKey ( key ) {
return `${ key }${ filterSeparator }${ this.filters[ key ] }`
}
isQueryValue ( filterNameOrQueryValue ) {
return filterNameOrQueryValue.includes( filterSeparator )
}
getKeyAndValue ( filterQueryValue ) {
const key = filterQueryValue.substring(0, filterQueryValue.indexOf( filterSeparator ))
const value = filterQueryValue.substring(filterQueryValue.indexOf( filterSeparator )+1)
return { key, value }
}
getFilterNameAndValueFromString ( filterNameOrQueryValue ) {
if ( this.isQueryValue( filterNameOrQueryValue ) ) {
return this.getKeyAndValue( filterNameOrQueryValue )
}
return {
key: filterNameOrQueryValue,
value: null
}
}
remove ( filterName ) {
// Throw error if it's not a valid filter name
if ( this.isQueryValue( filterName ) ) throw new Error(`${ filterName } is not a valid filter name`)
delete this.filters[ filterName ]
}
setFromStringArray ( filterStringArray ) {
filterStringArray.forEach( filterString => {
const { key, value } = this.getFilterNameAndValueFromString( filterString )
this.filters[ key ] = value
})
}
setFromString ( filterNameOrQueryValue ) {
const {
key,
value = ''
} = this.getFilterNameAndValueFromString( filterNameOrQueryValue )
// Throw for empty values
if ( value.trim().length === 0 ) throw new Error(`${ filterNameOrQueryValue } is not a valid filter value`)
this.set( key, value )
}
set ( filterName, filterValue ) {
// Throw error if it's not a valid filter name
if ( this.isQueryValue( filterName ) ) throw new Error(`${ filterName } is not a valid filter name`)
this.filters[ filterName ] = filterValue
}
toggleFilter ( filterNameOrQueryValue, filterValue = null ) {
const fromString = this.getFilterNameAndValueFromString( filterNameOrQueryValue )
const filterName = fromString.key
filterValue = filterValue || fromString.value
// If the filter is already set, remove it
if ( this.has( filterNameOrQueryValue ) ) {
this.remove( filterName )
return
}
// Throw error if filter value is not a string
if ( typeof filterValue !== 'string' ) {
throw new Error(`Filter value must be a string. Got ${ typeof filterValue }`)
}
this.set( filterName, filterValue )
}
has ( filterNameOrQueryValue ) {
const {
key : filterName,
value : filterValue = null
} = this.getFilterNameAndValueFromString( filterNameOrQueryValue )
// If this filter is a name and value, check if it's set
if ( isString( filterValue ) ) {
return !!this.filters[ filterName ] && this.filters[ filterName ] === filterValue
}
return !!this.filters[ filterName ]
}
}

13
helpers/stork/config.js Normal file
View file

@ -0,0 +1,13 @@
import { config } from '~/package.json'
export const storkOptions = config.stork
export const storkVersion = '1.4.2'
export const storkExecutableName = storkOptions.executable
export const storkExecutablePath = `./${ storkExecutableName }`
export const storkTomlPath = storkOptions.toml
export const storkIndexPath = storkOptions.index
export const storkIndexRelativeURL = storkIndexPath.replace('static/', '/')
export const storkScriptURL = `https://files.stork-search.net/releases/v${ storkVersion }/stork.js`

View file

@ -0,0 +1,95 @@
// Stork and Netlify - https://stork-search.net/docs/stork-and-netlify
import fs from 'fs-extra'
import execa from 'execa'
import { isDarwin } from '~/helpers/environment.js'
import {
storkVersion,
storkExecutableName,
storkExecutablePath,
storkTomlPath,
storkIndexPath
} from '~/helpers/stork/config.js'
// https://stork-search.net/docs/install
const execDownloadUrls = {
darwin: `https://files.stork-search.net/releases/v${ storkVersion }/stork-macos-10-15`,
default: `https://files.stork-search.net/releases/v${ storkVersion }/stork-ubuntu-20-04`
// default: `https://files.stork-search.net/releases/v${ storkVersion }/stork-amazon-linux`
}
// Check if a file is executable
// https://stackoverflow.com/a/69897809/1397641
async function isExecutable ( path ) {
const stats = await fs.stat( path )
const isExecutable = !!(stats.mode & fs.constants.S_IXUSR)
return isExecutable
}
// 👩‍💻 Bash Download example - https://github.com/jmooring/hugo-stork/blob/main/build.sh
export async function downloadStorkExecutable () {
const envKey = isDarwin() ? 'darwin' : 'default'
const execDownloadUrl = execDownloadUrls[ envKey ]
// console.log( 'execDownloadUrl', execDownloadUrl )
// Delete any existing executable
// so we don't get write errors
// or false positives from preexisting executable files
await fs.remove( storkExecutablePath )
// Download the binary
await execa( `curl`, [
execDownloadUrl,
// Set filename
'-o',
storkExecutableName
])
// Set the downloaded binary as executable
await fs.chmod( storkExecutablePath, '755' )
// Check that our downloaded binary is executable
// console.log( 'isExecutable', isExecutable )
if ( !isExecutable( storkExecutablePath ) ) throw new Error( `Downloaded binary at ${ storkExecutablePath } is not executable.` )
// Check Stork version
// so we know our binary is working
const { stdout } = await execa( storkExecutablePath, [
'--version'
])
console.log( 'Stork Version', stdout )
if ( !stdout.includes( storkVersion ) ) throw new Error( 'Stork --version command failed.' )
return stdout
}
export async function buildIndex () {
if ( !isExecutable( storkExecutablePath ) ) throw new Error( `Binary at ${ storkExecutablePath } is not executable.` )
// Check Stork version
// so we know our binary is working
const { stdout } = await execa( storkExecutablePath, [
'build',
'--input',
storkTomlPath,
'--output',
storkIndexPath
])
console.log( 'Stork Build', stdout )
if ( !stdout.includes( storkVersion ) ) throw new Error( 'Stork --version command failed.' )
return stdout
}

138
helpers/stork/toml.js Normal file
View file

@ -0,0 +1,138 @@
// import execa from 'execa'
import fs from 'fs-extra'
import has from 'just-has'
import TOML from '@iarna/toml'
import * as matter from 'gray-matter'
import {
isNonEmptyString,
isNonEmptyArray
} from '~/helpers/check-types.js'
import {
getRouteType
} from '~/helpers/app-derived.js'
import {
makeCategoryFilterFromListing
} from '~/helpers/categories.js'
import {
storkTomlPath,
} from '~/helpers/stork/config.js'
function makeDetailsFromListing ({ listing, route }) {
const propertiesToCheck = {
text: isNonEmptyString,
content: isNonEmptyString,
description: isNonEmptyString,
// status: isNonEmptyString,
aliases: isNonEmptyArray,
tags: isNonEmptyArray,
}
const contents = {}
for ( const [ property, isValid ] of Object.entries( propertiesToCheck ) ) {
if ( !has( listing, property ) ) continue
if ( !isValid( listing[ property ] ) ) continue
let value = listing[ property ]
// Convert arrays to string
if ( Array.isArray( value ) ) {
value = value.join(', ')
}
// Property can be added to content
contents[ property ] = value
}
return [
listing.content || '∅', // Null Symbol
has( listing, 'status' ) ? `status_${ listing.status }` : '',
has( listing, 'category' ) ? makeCategoryFilterFromListing( listing ) : '',
`type_${ getRouteType( route ) }`,
// Brownmatter
matter.stringify( '', contents ),
].join('\r\n')
}
function mapSitemapEndpointsToToml ( sitemap ) {
const files = sitemap.map( sitemapEntry => {
const {
payload,
route
} = sitemapEntry
const routeType = getRouteType( route )
// console.log( 'payload', route, payload )
const listing = payload.app || payload.listing || payload.video || {}
const contents = makeDetailsFromListing({ listing, route })
let title = listing.name || route
// If this route is a benchmark route, add the benchmark name
if ( routeType === 'benchmarks' ) {
title = `${ title } Benchmarks`
}
// console.log( 'listing', listing )
// console.log( 'contents', contents )
// console.log( 'name', listing.name )
if ( contents.trim().length === 0 ) {
console.log( 'listing', listing )
throw new Error('Empty Content')
}
return {
// https://stork-search.net/docs/config-ref#title
title,
url: route,
contents
}
})
return {
input: {
// https://stork-search.net/docs/config-ref#base_directory
base_directory: '.',
url_prefix: 'https://doesitarm.com',
// https://stork-search.net/docs/config-ref#files
files
},
output: {
// debug: true,
// save_nearest_html_id: false,
// excerpt_buffer: 8,
// excerpts_per_result: 5,
displayed_results_count: 100,
}
}
}
export async function writeStorkToml ( sitemap ) {
const indexToml = mapSitemapEndpointsToToml( sitemap )
// Build Stork Index TOML
// https://stork-search.net/docs/config-ref
const indexString = TOML.stringify( indexToml )
// Save to file
await fs.outputFile( storkTomlPath, indexString )
return
}

View file

@ -1,12 +1,20 @@
import { getSiteUrl } from './url'
function makeFeaturedAppsString ( featuredApps ) {
return featuredApps.slice(0, 5).map(app => app.name).join(', ')
}
export function buildVideoStructuredData ( video, featuredApps, options ) {
export function buildVideoStructuredData ( video, featuredApps, options = {} ) {
// console.log('video', video)
// Throw for missing featured apps
if ( Array.isArray(featuredApps) === false ) {
console.warn( 'featuredApps not array', featuredApps )
throw new Error('featuredApps must be an array of objects')
}
const {
siteUrl
siteUrl = getSiteUrl(),
} = options
const thumbnailUrls = video.thumbnail.srcset.split(',').map( srcSetImage => {

92
helpers/url.js Normal file
View file

@ -0,0 +1,92 @@
export function getSiteUrl () {
// console.log( 'import.meta.site', import.meta.env )
const hasImportMeta = typeof import.meta !== 'undefined'
const hasImportMetaEnv = hasImportMeta && typeof import.meta.env !== 'undefined'
// Try PUBLIC_URL
if ( typeof process.env.PUBLIC_URL !== 'undefined' ) {
console.log('Has env.PUBLIC_URL')
return process.env.PUBLIC_URL
}
if ( hasImportMetaEnv && typeof import.meta.env.PUBLIC_URL !== 'undefined' ) {
console.log('Has PUBLIC_URL')
return import.meta.env.PUBLIC_URL
}
// Try process.env.URL
if ( typeof process.env.URL !== 'undefined' ) {
console.log('Has env.URL')
return process.env.URL
}
// Try Astro.site.origin
if ( typeof Astro !== 'undefined' ) {
console.log('Has Astro')
return Astro.site.origin
}
// Try URL
if ( hasImportMetaEnv && typeof import.meta.env.URL !== 'undefined' ) {
console.log('Has URL')
return import.meta.env.URL
}
throw new Error('Could not find site URL')
}
export function getApiUrl () {
const hasImportMeta = typeof import.meta !== 'undefined'
const hasImportMetaEnv = hasImportMeta && typeof import.meta.env !== 'undefined'
// Try PUBLIC_API_DOMAIN
if ( typeof process.env.PUBLIC_API_DOMAIN !== 'undefined' ) {
// console.log('Has env.PUBLIC_API_DOMAIN')
return process.env.PUBLIC_API_DOMAIN
}
if ( hasImportMetaEnv && typeof import.meta.env.PUBLIC_API_DOMAIN !== 'undefined' ) {
// console.log('Has PUBLIC_API_DOMAIN')
return import.meta.env.PUBLIC_API_DOMAIN
}
throw new Error('Could not find API URL')
}
export function getPartPartsFromUrl ( urlString ) {
if ( typeof urlString !== 'string' ) throw new Error('urlString must be a string')
const url = new URL( urlString, 'https://doesitarm.com' )
const pathParts = url.pathname
.replace(/^\/+/, '') // Trim slashes from the beginning
.replace(/\/+$/, '') // Trim slashes from the end
.split('/')
return pathParts
}
export function getPathPartsFromAstroRequest ( AstroRequest ) {
// Parse the request url
const url = new URL( AstroRequest.url, 'https://doesitarm.com' )
const [
routeType,
pathSlug,
subSlug
] = getPartPartsFromUrl ( AstroRequest.url )
return {
pathname: url.pathname,
routeType,
pathSlug,
subSlug,
params: Object.fromEntries( url.searchParams )
}
}

74
layouts/base.vue Normal file
View file

@ -0,0 +1,74 @@
<template>
<div class="app-wrapper text-gray-300 bg-gradient-to-bl from-dark to-darker bg-fixed">
<Navbar />
<div class="app-main min-h-screen flex items-center">
<slot />
</div>
<footer class="">
<div class="max-w-screen-xl mx-auto py-12 px-4 overflow-hidden space-y-24 sm:px-6 lg:px-8">
<!-- <nav class="-mx-5 -my-2 flex flex-wrap justify-center">
<div class="px-5 py-2">
<a href="#" class="text-base leading-6 text-gray-500 hover:text-gray-900">
About
</a>
</div>
<div class="px-5 py-2">
<a href="#" class="text-base leading-6 text-gray-500 hover:text-gray-900">
Blog
</a>
</div>
</nav> -->
<div class="flex justify-center space-x-6">
<div class="flex flex-col items-center space-y-4">
<AllUpdatesSubscribe />
</div>
</div>
<p class="text-center text-base leading-6 text-gray-400">
<span>Built by </span>
<a
href="https://samcarlton.com/"
rel="noopener"
class="underline"
>Sam Carlton</a>
<span> and the awesome </span>
<a
href="https://github.com/ThatGuySam/doesitarm/graphs/contributors"
rel="noopener"
class="underline"
>🦾 Does It ARM Contributors. </a>
</p>
<p class="mt-8 text-center text-base leading-6 text-gray-400">
&copy; {{ currentYear }} Does It ARM All rights reserved. This site is supported by Affiliate links.
</p>
</div>
</footer>
</div>
</template>
<script>
// import '@fontsource/inter/latin-100.css'
// import '@fontsource/inter/latin-400.css'
// import '@fontsource/inter/latin-700.css'
import '@fontsource/inter/variable.css'
import Navbar from '~/components/navbar.vue'
// import TwitterFollow from '~/components/twitter-follow.vue'
import AllUpdatesSubscribe from '~/components/all-updates-subscribe.vue'
export default {
components: {
Navbar,
AllUpdatesSubscribe
},
computed: {
currentYear () {
return new Date().getFullYear()
}
}
}
</script>

View file

@ -1,74 +1,17 @@
<template>
<div class="app-wrapper text-gray-300 bg-gradient-to-bl from-dark to-darker bg-fixed">
<Navbar />
<div class="app-main min-h-screen flex items-center">
<nuxt />
</div>
<footer class="">
<div class="max-w-screen-xl mx-auto py-12 px-4 overflow-hidden space-y-24 sm:px-6 lg:px-8">
<!-- <nav class="-mx-5 -my-2 flex flex-wrap justify-center">
<div class="px-5 py-2">
<a href="#" class="text-base leading-6 text-gray-500 hover:text-gray-900">
About
</a>
</div>
<div class="px-5 py-2">
<a href="#" class="text-base leading-6 text-gray-500 hover:text-gray-900">
Blog
</a>
</div>
</nav> -->
<div class="flex justify-center space-x-6">
<div class="flex flex-col items-center space-y-4">
<AllUpdatesSubscribe />
</div>
</div>
<p class="text-center text-base leading-6 text-gray-400">
<span>Built by </span>
<a
href="https://samcarlton.com/"
rel="noopener"
class="underline"
>Sam Carlton</a>
<span> and the awesome </span>
<a
href="https://github.com/ThatGuySam/doesitarm/graphs/contributors"
rel="noopener"
class="underline"
>🦾 Does It ARM Contributors. </a>
</p>
<p class="mt-8 text-center text-base leading-6 text-gray-400">
&copy; {{ currentYear }} Does It ARM All rights reserved. This site is supported by Affiliate links.
</p>
</div>
</footer>
<div>
<Base>
<nuxt />
</Base>
</div>
</template>
<script>
// import '@fontsource/inter/latin-100.css'
// import '@fontsource/inter/latin-400.css'
// import '@fontsource/inter/latin-700.css'
import '@fontsource/inter/variable.css'
import Navbar from '~/components/navbar.vue'
// import TwitterFollow from '~/components/twitter-follow.vue'
import AllUpdatesSubscribe from '~/components/all-updates-subscribe.vue'
import Base from '~/layouts/base.vue'
export default {
components: {
Navbar,
AllUpdatesSubscribe
},
computed: {
currentYear () {
return new Date().getFullYear()
}
Base
}
}
</script>

View file

@ -1,7 +1,26 @@
[build]
publish = "dist/"
command = "npm run generate --quiet"
# functions = "functions/"
command = "npm run netlify-build"
# https://docs.netlify.com/configure-builds/file-based-configuration/#functions
[functions]
# Sets a custom directory for Netlify Functions
directory = "dist/functions"
# Specifies `esbuild` for functions bundling
# node_bundler = "esbuild"
# Flags "astro" as an external node module for all functions
# external_node_modules = ["astro"]
# Includes all Markdown files inside the "files/" directory.
# included_files = ["files/*.md"]
# Astro Entry function
[functions."entry"]
# https://www.netlify.com/blog/2021/08/12/how-to-include-files-in-netlify-serverless-functions/
included_files = ["netlify.toml"]
@ -9,9 +28,14 @@
NPM_FLAGS = "--no-optional"
CI = "1"
[[headers]]
for = "/search-index.st"
[headers.values]
Content-Type = "application/wasm"
Cache-Control = "max-age=43200, must-revalidate"
# https://docs.netlify.com/configure-builds/file-based-configuration/#redirects
# These redirect are lower priority than _redirects in the root or dist
# old node redirect
[[redirects]]
from = "/app/node"

View file

@ -1,17 +1,18 @@
import { promises as fs } from 'fs'
import pkg from './package'
import pkg from './package.json'
import { getSiteUrl } from '~/helpers/url.js'
import { publicRuntimeConfig } from '~/helpers/public-runtime-config.mjs'
const siteUrl = getSiteUrl()
export default {
target: 'static',
publicRuntimeConfig: {
allUpdateSubscribe: process.env.ALL_UPDATE_SUBSCRIBE,
testResultStore: process.env.TEST_RESULT_STORE,
siteUrl: process.env.URL,
...pkg.config
},
publicRuntimeConfig,
/*
** Hooks
@ -42,107 +43,7 @@ export default {
/*
** Headers of the page
*/
head: {
// this htmlAttrs you need
htmlAttrs: {
lang: 'en',
},
title: 'Does It ARM',
description: process.env.npm_package_config_verbiage_description,
meta: [
{ charset: 'utf-8' },
{
name: 'viewport',
content: 'width=device-width, initial-scale=1'
},
{
hid: 'description',
name: 'description',
content: process.env.npm_package_config_verbiage_description
},
{
'property': 'og:image',
'content': `${process.env.URL}/images/og-image.png`
},
{
'property': 'og:image:width',
'content': '1200'
},
{
'property': 'og:image:height',
'content': '627'
},
{
'property': 'og:image:alt',
'content': 'Does It ARM Logo'
},
// Twitter Card
{
'property': 'twitter:card',
'content': 'summary'
},
{
'property': 'twitter:title',
'content': 'Does It ARM'
},
// {
// 'property': 'twitter:description',
// 'content': process.env.npm_package_config_verbiage_description
// },
{
'property': 'twitter:url',
'content': `${process.env.URL}`
},
{
'property': 'twitter:image',
'content': `${process.env.URL}/images/mark.png`
}
],
link: [
// Favicon
{
rel: 'icon',
type: 'image/x-icon',
href: '/favicon.ico'
},
// Gtag Preconnect
{
rel: 'preconnect',
href: 'https://www.googletagmanager.com'
},
// Carbon Preconnects
{
rel: 'preconnect',
href: 'https://cdn.carbonads.com'
},
{
rel: 'preconnect',
href: 'https://srv.carbonads.net'
},
{
rel: 'preconnect',
href: 'https://cdn4.buysellads.net'
},
],
script: [
// // Carbon Ads
// // https://sell.buysellads.com/zones/1294/ad-tags#z=js
// {
// // <script async type="text/javascript" src="//cdn.carbonads.com/carbon.js?serve=CK7DVK3M&placement=doesitarmcom" id="_carbonads_js"></script>
// src: 'https://cdn.carbonads.com/carbon.js?serve=CK7DVK3M&placement=doesitarmcom',
// async: true,
// type: 'text/javascript',
// id: '_carbonads_js',
// class: 'include-on-static carbon-inline-wide',
// body: true
// }
]
},
head: ,
/*
** Customize the progress-bar color

14030
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -4,20 +4,25 @@
"description": "Find out the latest app support for Apple Silicon and the Apple M2 and M1 Ultra Processors",
"author": "Sam Carlton",
"private": true,
"ava": {
"require": [
"esm"
]
},
"config": {
"verbiage": {
"processors": "Apple M2 and M1 Ultra",
"macs": "Apple M2 or M1 Ultra Mac",
"description": "Find out the latest app support for Apple Silicon and the Apple M2 and M1 Ultra Processors"
},
"stork": {
"executable": "stork-executable",
"toml": "static/stork.toml",
"index": "static/search-index.st"
}
},
"scripts": {
"test-prebuild": "ava ./test/prebuild.js --verbose",
"test-prebuild": "ava ./test/prebuild/**/*.js --verbose",
"test-api-client": "ava ./test/api/client.js --verbose",
"test-listings": "ava ./test/listings/**/*.js --verbose",
"test-prebuild-functions": "run-s test-prebuild test-api-client test-listings",
"test-postbuild-api": "run-s test-listings",
"test-postbuild-functions": "ava ./test/main.js --verbose",
"test": "ava --timeout=1m --verbose",
"dev": "nuxt",
"build": "nuxt build",
@ -26,7 +31,18 @@
"start": "nuxt start",
"generate-dev": "npm run generate && npm test",
"generate": "npm run clone-readme && npm run build-lists && npm run generate-nuxt && npm run generate-eleventy",
"build-lists": "npm run test-prebuild && node -r esm build-lists.js",
"build-lists": "npm run test-prebuild && node -r esm -r tsconfig-paths/register build-lists.js",
"setup-stork": "run-s download-stork-toml download-stork-executable",
"build-stork-index": "./$npm_package_config_stork_executable build --input $npm_package_config_stork_toml --output $npm_package_config_stork_index",
"build-stork-index-js": "node -r esm -r tsconfig-paths/register scripts/build-stork-index.js",
"stork-search": "./$npm_package_config_stork_executable search --index $npm_package_config_stork_index --query $1",
"stork-index": "run-s setup-stork build-stork-index",
"download-stork-toml": "node -r esm -r tsconfig-paths/register scripts/download-stork-toml.js",
"download-stork-executable": "node -r esm -r tsconfig-paths/register scripts/download-stork-executable.js",
"download-sitemaps": "node -r esm -r tsconfig-paths/register scripts/download-sitemaps.js",
"stork-netlify": "chmod +x scripts/stork-netlify.sh && ./scripts/stork-netlify.sh",
"dev-astro": "astro dev",
"generate-astro": "astro build --experimental-ssr",
"generate-nuxt": "NODE_OPTIONS=--max-old-space-size=60000 nuxt generate",
"generate-eleventy": "node --max-old-space-size=60000 -r esm node_modules/.bin/eleventy --quiet",
"generate-postcss": "ENV=production postcss assets/css/tailwind.css --o static/tailwind.css",
@ -39,36 +55,60 @@
"precommit": "npm run lint",
"clone-readme": "cp ./README.md README-temp.md",
"cloudflare-deploy": "npm run build-api",
"vercel-build": "npm run build-lists-and-api"
"vercel-build": "run-s build-lists-and-api test-postbuild-api",
"netlify-build": "run-s test-prebuild-functions download-sitemaps stork-index generate-astro test-postbuild-functions"
},
"dependencies": {
"@11ty/eleventy-assets": "^1.0.5",
"@astrojs/partytown": "^0.1.4",
"@astrojs/vue": "^0.1.3",
"@fontsource/inter": "^4.0.1",
"@iarna/toml": "^2.2.5",
"@nuxtjs/sitemap": "^2.4.0",
"@open-wc/webpack-import-meta-loader": "^0.4.7",
"@originjs/vite-plugin-commonjs": "^1.0.3",
"@supercharge/promise-pool": "^2.1.0",
"@zip.js/zip.js": "^2.2.6",
"astro": "^1.0.0-beta.27",
"axios": "^0.21.0",
"can-autoplay": "^3.0.0",
"chance": "^1.1.7",
"cross-env": "^5.2.0",
"esbuild": "^0.11.20",
"execa": "^5.1.1",
"fast-glob": "^3.2.11",
"fast-memoize": "^2.5.2",
"fs-extra": "^8.1.0",
"googleapis": "^100.0.0",
"gray-matter": "^4.0.3",
"jsdom": "^16.4.0",
"just-has": "^2.1.0",
"just-map-values": "^3.0.2",
"just-replace-all": "^2.0.1",
"just-shuffle": "^4.0.1",
"lazysizes": "^5.3.0-beta1",
"markdown-it": "^11.0.1",
"marked": "^1.2.7",
"memoize-getters": "^1.1.0",
"node-html-parser": "^2.0.0",
"observe-element-in-viewport": "0.0.15",
"plist": "^3.0.1",
"pretty-bytes": "^5.5.0",
"scroll-into-view-if-needed": "^2.2.26",
"semver": "^7.3.7",
"sitemap": "^7.1.1",
"slugify": "^1.4.6",
"stork-search": "^1.0.4",
"terser": "^4.8.0",
"vue-gtag": "^1.16.1"
"uuid": "^8.3.2",
"vue": "^3.2.30",
"vue-gtag": "^2.0.1"
},
"devDependencies": {
"@11ty/eleventy": "^0.11.1",
"@astrojs/netlify": "0.3.2",
"@astrojs/sitemap": "^0.1.0",
"@astrojs/tailwind": "^0.2.0",
"@nuxt/postcss8": "^1.1.3",
"@nuxtjs/tailwindcss": "^3.3.4",
"autoprefixer": "^8.6.4",
@ -76,21 +116,19 @@
"babel-eslint": "^8.2.1",
"cssnano": "^4.1.10",
"dotenv": "^8.2.0",
"eslint": "^5.16.0",
"eslint-config-prettier": "^3.1.0",
"eslint-loader": "^2.0.0",
"eslint-plugin-prettier": "2.6.2",
"eslint-plugin-vue": "^4.0.0",
"eslint": "^8.14.0",
"eslint-plugin-vue": "^8.7.1",
"esm": "^3.2.25",
"fast-xml-parser": "^3.19.0",
"node-fetch": "^2.6.1",
"nodemon": "^1.11.0",
"npm-run-all": "^4.1.5",
"nuxt": "^2.14.11",
"postcss": "^8.2.4",
"postcss-cli": "^8.3.1",
"prettier": "1.14.3",
"replace-css-url": "^1.2.6",
"structured-data-testing-tool": "^4.5.0",
"tailwindcss": "^1.9.6"
"tailwindcss": "^1.9.6",
"tsconfig-paths": "^3.14.1"
}
}

View file

@ -173,6 +173,7 @@
// import AppFilesScanner from '~/helpers/app-files-scanner.js'
import { isNuxt } from '~/helpers/environment.js'
import LinkButton from '~/components/link-button.vue'
import AllUpdatesSubscribe from '~/components/all-updates-subscribe.vue'
@ -191,6 +192,12 @@ export default {
LinkButton,
AllUpdatesSubscribe
},
props: {
config: {
type: Object,
default: null
}
},
data: function () {
return {
query: '',
@ -200,7 +207,7 @@ export default {
},
computed: {
npm_package_config_verbiage_macs () {
return process.env.npm_package_config_verbiage_macs
return this.config.macsVerbiage //process.env.npm_package_config_verbiage_macs
},
foundFiles () {
return this.appsBeingScanned.filter( appScan => {
@ -264,7 +271,7 @@ export default {
return `Apple Silicon Compatibility Test Online`
},
description () {
return `Check for Apple Silicon compatibility for any of your apps instantly before you buy an ${ process.env.npm_package_config_verbiage_macs }. `
return `Check for Apple Silicon compatibility for any of your apps instantly before you buy an ${ this.$config.macsVerbiage }. `
}
},
mounted () {
@ -295,13 +302,18 @@ export default {
console.log('Initializing scanner instance')
// Bring in code
const { default: AppFilesScanner} = await import('~/helpers/app-files-scanner.js')
const { default: AppFilesScanner } = await import('~/helpers/app-files-scanner.js')
const testResultStore = this.config ? this.$config.testResultStore : this.$config.testResultStore
// Initialize instance
this.scanner = new AppFilesScanner({
observableFilesArray: this.appsBeingScanned,
testResultStore: this.$config.testResultStore
testResultStore
})
// Setup scanner
await this.scanner.setup()
}
// console.log('fileInputChanged files', fileList)

11
sandbox.config.json Normal file
View file

@ -0,0 +1,11 @@
{
"infiniteLoopProtection": true,
"hardReloadOnChange": false,
"view": "browser",
"template": "node",
"container": {
"port": 3000,
"startScript": "start",
"node": "14"
}
}

View file

@ -0,0 +1,12 @@
import {
downloadStorkExecutable,
buildIndex
} from '~/helpers/stork/executable.js'
;(async () => {
await downloadStorkExecutable()
await buildIndex()
process.exit()
})()

View file

@ -0,0 +1,55 @@
import fs from 'fs-extra'
import 'dotenv/config'
import axios from 'axios'
import {
sitemapLocation,
sitemapIndexFileName,
} from '~/helpers/constants.js'
import { parseSitemapXml } from '~/helpers/api/sitemap/parse.js'
;(async () => {
// Build Sitemap Index URL
const sitemapIndexUrl = new URL( `${ sitemapLocation.split('static')[1] }${ sitemapIndexFileName }`, process.env.PUBLIC_API_DOMAIN )
// Fetch Sitemap Index
const sitemapIndexXML = await axios.get( sitemapIndexUrl.href ).then( response => response.data )
// Save Sitemap Index
const sitemapIndexFilePath = `${ sitemapLocation }${ sitemapIndexFileName }`
await fs.writeFile( sitemapIndexFilePath, sitemapIndexXML )
const urlEntries = parseSitemapXml( sitemapIndexXML )
// Fetch each sitemap
for ( const entry of urlEntries ) {
// Build Sitemap Index URL
const sitemapUrl = new URL( entry.loc )
const apiSitemapUrl = new URL( sitemapUrl.pathname, process.env.PUBLIC_API_DOMAIN )
// sitemapUrl.origin = process.env.PUBLIC_API_DOMAIN
// Fetch Sitemap Index
const sitemapXML = await axios.get( apiSitemapUrl.href ).then( response => response.data )
// const sitemap = parse( sitemapXML )
// console.log( 'sitemap', sitemap )
// console.log( 'apiSitemapUrl', apiSitemapUrl )
const sitemapFileName = apiSitemapUrl.pathname.split('/')[1]
const sitemapIndexFilePath = `${ sitemapLocation }${ sitemapFileName }`
// Save file
await fs.writeFile( sitemapIndexFilePath, sitemapXML )
}
process.exit()
})()

View file

@ -0,0 +1,7 @@
import { downloadStorkExecutable } from '~/helpers/stork/executable.js'
;(async () => {
await downloadStorkExecutable()
process.exit()
})()

View file

@ -0,0 +1,9 @@
import {
downloadStorkToml
} from '~/helpers/api/static.js'
;(async () => {
await downloadStorkToml()
process.exit()
})()

12
scripts/stork-netlify.sh Executable file
View file

@ -0,0 +1,12 @@
#!/usr/bin/env bash
# Hugo Bash Example https://github.com/jmooring/hugo-stork/blob/main/build.sh
# curl https://files.stork-search.net/releases/latest/stork-amazon-linux -o stork-executable
# curl https://files.stork-search.net/releases/v1.4.3/stork-macos-latest -o stork-executable
curl https://files.stork-search.net/releases/v1.4.2/stork-amazon-linux -o stork-executable
# curl https://files.stork-search.net/releases/v1.4.2/stork-macos-10-15 -o stork-executable
chmod +x stork-executable
./stork-executable build --input static/stork.toml --output static/search-index.st

View file

@ -0,0 +1,91 @@
---
// Default Listing template for Apps, Games, and formulas
import {
ListingDetails
} from '~/helpers/listing-page.js'
import Aliases from '~/src/components/listing-parts/aliases.astro'
import ThomasCredit from '~/components/thomas-credit.vue'
import RelatedLinks from '~/src/components/listing-parts/related-links.astro'
import Virtualization from './listing-parts/virtualization.astro'
import CarbonInline from '~/components/carbon-inline.vue'
import Devices from '~/src/components/listing-parts/devices.astro'
import RelatedVideos from '~/src/components/listing-parts/related-videos.astro'
import Bundles from '~/src/components/listing-parts/bundles.astro'
import GameReports from '~/src/components/listing-parts/game-reports.astro'
import LastUpdated from '~/src/components/listing-parts/last-updated.astro'
import AllUpdatesSubscribe from '~/components/all-updates-subscribe.vue'
// import { makeLastUpdatedFriendly } from '~/helpers/parse-date'
// import { getAppEndpoint } from '~/helpers/app-derived.js'
// import LinkButton from '~/components/link-button.vue'
// import VideoRow from '~/components/video/row.vue'
const {
listing
} = Astro.props
const details = new ListingDetails( listing )
---
<section class="container space-y-8 py-32">
<div class="intro-content flex flex-col items-center text-center min-h-3/4-screen md:min-h-0 gap-8">
<div
class="title text-sm md:text-xl font-bold"
set:html={ details.mainHeading }
/>
<h2 class="subtitle text-2xl md:text-5xl font-bold">
{ details.subtitle }
</h2>
{ details.isGame && (
<ThomasCredit />
)}
<Aliases listing={ listing } />
<!-- <AllUpdatesSubscribe
client:visible
/> -->
<div class="links space-y-6 sm:space-x-6">
<RelatedLinks
listing={ listing }
/>
</div>
<Virtualization listing={ listing } />
<CarbonInline class="carbon-inline-wide w-full" />
<Devices
listing={ listing }
/>
<RelatedVideos
listing={ listing }
/>
</div>
<Bundles
listing={ listing }
/>
<GameReports
listing={ listing }
/>
<LastUpdated
listing={ listing }
/>
</section>

View file

@ -0,0 +1,28 @@
---
import { gaMeasurementId } from '~/helpers/constants.js'
// const gaMeasurementId = 'G-0WLH5YTTB0'
---
<!-- Set gaMeasurementId so it's available within the browser/window context -->
<script
type="text/javascript"
set:html={ `window.gaMeasurementId = '${ gaMeasurementId }'` }
></script>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script
type="text/partytown"
async
src={ `https://www.googletagmanager.com/gtag/js?id=${ gaMeasurementId }` }
></script>
<script type="text/partytown">
window.dataLayer = window.dataLayer || [];
function gtag () {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', gaMeasurementId);
// console.log('I wanna to shake your hand', gaMeasurementId )
</script>

View file

@ -0,0 +1,14 @@
---
const {
listing
} = Astro.props
const hasMultipleAliases = Array.isArray( listing.aliases ) && listing.aliases.length > 1
const listFormatter = new Intl.ListFormat('en', { style: 'short', type: 'disjunction' })
---
{ hasMultipleAliases ?
<small class="text-xs opacity-75">May also be known as { listFormatter.format( listing.aliases, 'or' ) }</small>
: '' }

View file

@ -0,0 +1,105 @@
---
// https://docs.astro.build/en/reference/api-reference/#code-
// import { Code } from 'astro/components'
import { getStatusOfScan } from '~/helpers/statuses.js'
import { supportedArchitectures } from '~/helpers/bundles.js'
import Heading from './heading.astro'
const {
listing
} = Astro.props
const hasBundleIdentifiers = Array.isArray( listing.bundles ) && listing.bundles.length > 0
---
{ hasBundleIdentifiers && (
<div class="app-bundles w-full">
<Heading text='Bundle Version History' />
<div class="app-bundles-container border rounded-lg">
<div class="app-bundles-list md:inline-flex w-full overflow-x-auto overflow-y-visible md:whitespace-no-wrap divide-y md:divide-y-0 md:divide-x divide-gray-700 space-y-3 md:space-y-0 py-4 px-3">
{ listing.bundles.map( ( [ bundleIdentifier ] ) => (
<div class="bundle-listing-container w-full md:w-auto inline-flex flex-col space-y-2 px-2">
<a
href={ `#bundle_identifier=${bundleIdentifier}` }
role="button"
class="bundle-link block rounded-md text-sm font-medium leading-5 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 p-2"
aria-label={ bundleIdentifier }
>{ bundleIdentifier }</a>
</div>
)) }
</div>
<div class="app-bundle-detail-view space-y-12 py-6 md:px-5">
{ listing.bundles.map( ( [ bundleIdentifier, versions ] ) => {
return (
<div
id={ `bundle_identifier=${bundleIdentifier}` }
class="bundle-detail-container w-full overflow-hidden space-y-2 px-2"
>
<h3
class="md:text-2xl font-bold"
>{ bundleIdentifier }</h3>
<div class="bundle-versions-container border rounded-lg bg-black bg-opacity-10">
<div class="app-bundles-list md:inline-flex w-full overflow-x-auto overflow-y-visible md:whitespace-no-wrap divide-y md:divide-y-0 md:divide-x divide-gray-700 space-y-3 md:space-y-0 py-4 px-3">
{ versions.map( ( [ version, report ] ) => (
<div
class="bundle-listing-container w-full md:w-auto inline-flex flex-col p-4"
style="min-width: 300px;"
>
<div class="version-heading font-bold text-xl">v{ version }</div>
<div class="version-body divide-y-0 py-2">
<div class="version-status">
{ getStatusOfScan( report, false ) }
</div>
<div class="version-architecture">
🖥 Supported Architectures <span class="rounded bg-black bg-opacity-50 p-1">{ supportedArchitectures( report ).join(', ') }</span>
</div>
</div>
<details>
<summary
class="text-xs cursor-pointer hover:bg-black-7 rounded px-2 py-1"
>Full Info Plist</summary>
<pre
class="inline-block border-dashed border rounded-lg space-y-4 p-4 mt-4"
style="background-color: #0d1117"
>{ JSON.stringify( report['Info Plist'], undefined, 2) }</pre>
</details>
<details>
<summary
class="text-xs cursor-pointer hover:bg-black-7 rounded px-2 py-1"
>Full Meta Details</summary>
<pre
class="inline-block border-dashed border rounded-lg space-y-4 p-4 mt-4"
style="background-color: #0d1117"
>{ JSON.stringify( report['Macho Meta'], undefined, 2) }</pre>
</details>
</div>
)) }
</div>
</div>
</div>
)
}) }
</div>
</div>
</div>
)}

View file

@ -0,0 +1,33 @@
---
import Heading from './heading.astro'
const {
listing
} = Astro.props
const hasDeviceSupport = Array.isArray( listing.deviceSupport )
---
{ hasDeviceSupport && (
<div class="device-support w-full">
<Heading text='Device Support' />
<div class="device-support-apps md:inline-flex md:w-full max-w-4xl overflow-x-auto overflow-y-visible md:whitespace-no-wrap border rounded-lg divide-y md:divide-y-0 md:divide-x divide-gray-700 space-y-3 md:space-y-0 py-4 px-3">
{ listing.deviceSupport.map( device => (
<div class="device-container w-full md:w-auto inline-flex flex-col space-y-2 px-2">
<a
href={ device.endpoint }
role="button"
class="device-link block rounded-md text-sm font-medium leading-5 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 p-2"
aria-label={ device.ariaLabel }
>{ device.emoji } { device.name }</a>
<a href={ device.amazonUrl } target="_blank" class="underline text-xs pb-3" rel="noopener">Check Pricing</a>
</div>
)) }
</div>
</div>
)}

View file

@ -0,0 +1,89 @@
---
// import { getStatusOfScan } from '~/helpers/statuses.js'
// import { supportedArchitectures } from '~/helpers/bundles.js'
import { ensureListingDetails } from '~/helpers/listing-page'
import Heading from './heading.astro'
const {
listing
} = Astro.props
const details = ensureListingDetails(listing)
const hasReports = Array.isArray( details.api.reports ) && details.api.reports.length > 0
const defaultSourceUrl = 'https://applesilicongames.com/'
function getSourceUrl ( report ) {
// Try to get the source url from the report first
if ( report['Source'].includes('https://') ) {
return report['Source']
}
// Otherwise, fall back to the default source url
return defaultSourceUrl
}
---
{ hasReports && (
<Heading
text='Reports'
/>
<ul class="flex flex-col md:flex-row space-x-0 space-y-4 md:space-y-0 md:space-x-4 mb-4">
{ listing.reports.map( report => (
<li
class="col-span-1 rounded-lg border w-full md:w-64"
>
<div class="w-full flex items-center justify-between p-6">
<div class="flex-1">
<div class="space-x-3">
<h3 class="text-sm leading-5 font-bold">{ report['Specs'] }</h3>
<span class="flex-shrink-0 inline-block px-2 py-0.5 text-teal-800 text-xs leading-4 font-bold bg-teal-100 rounded-full">{ report['FPS'] }</span>
</div>
<p class="mt-1 text-sm leading-5">{ report['Notes'] }</p>
<p
v-if="report['Resolution'].length !== 0"
class="mt-1 text-sm leading-5"
>
🖥 { report['Resolution'] }
</p>
{ report['Settings'].length !== 0 && (
<p class="mt-1 text-sm leading-5">
⚙️ { report['Settings'] }
</p>
)}
</div>
</div>
<div class="border-t border-gray-200">
<div class="-mt-px flex">
<div class="w-0 flex-1 flex border-r border-gray-200">
<a
href={ getSourceUrl( report ) }
class="relative -mr-px w-0 flex-1 inline-flex items-center justify-center py-4 text-sm leading-5 font-bold border border-transparent rounded-bl-lg hover:text-gray-500 focus:outline-none focus:shadow-outline-blue focus:border-blue-300 focus:z-10 transition ease-in-out duration-150"
>
<!-- Heroicon name: mail -->
<svg
class="w-5 h-5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z" />
<path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z" />
</svg>
<span class="ml-3 opacity-75">Source</span>
</a>
</div>
</div>
</div>
</li>
))}
</ul>
)}

View file

@ -0,0 +1,13 @@
---
import { makeSlug } from '~/helpers/slug.js'
const {
text
} = Astro.props
---
<h2
id={ makeSlug( text ) }
class="section-heading text-xl md:text-2xl text-center font-bold mb-3"
>
{ text }
</h2>

View file

@ -0,0 +1,37 @@
---
import { ensureListingDetails } from '~/helpers/listing-page.js'
import { makeLastUpdatedFriendly } from '~/helpers/parse-date.js'
const {
listing
} = Astro.props
const details = ensureListingDetails( listing )
const lastUpdatedFriendly = makeLastUpdatedFriendly( listing.lastUpdated )
const gameReportUrl = 'https://forms.gle/29GWt85i1G1L7Ttj8'
const defaultReportUrl = `https://github.com/ThatGuySam/doesitarm/issues?q=is%3Aissue+${ listing.name }`
const reportUrl = details.isGame ? gameReportUrl : defaultReportUrl
---
<div class="report-update text-xs text-center w-full shadow-none py-24">
{ lastUpdatedFriendly !== null &&
<div>
<time
datetime={ listing.lastUpdated.raw }
>
Last Updated { lastUpdatedFriendly }
</time>
</div>
}
<!-- https://eric.blog/2016/01/08/prefilling-github-issues/ -->
<a
href={ reportUrl }
target="_blank"
class="underline"
rel="noopener"
>Report Update</a>
</div>

View file

@ -0,0 +1,23 @@
---
const {
listing
} = Astro.props
const hasRelatedLinks = Array.isArray(listing.relatedLinks)
---
{ hasRelatedLinks && listing.relatedLinks.map( (link, i) => {
const notAppTestLink = !link.label.includes('🧪')
const isMainLink = (i === 0) && notAppTestLink
return (
<a
class="relative inline-flex items-center rounded-md px-4 py-2 leading-5 font-bold text-white border border-transparent focus:outline-none focus:border-indigo-600 neumorphic-shadow focus:shadow-outline-indigo bg-darker hover:bg-indigo-400 active:bg-indigo-600 transition duration-150 ease-in-out"
href={ link.href }
target="_blank"
rel="noopener"
role="button"
>{ isMainLink ? 'View' : link.label }</a>
)
} ) }

View file

@ -0,0 +1,24 @@
---
import { ensureListingDetails } from '~/helpers/listing-page.js'
import Heading from './heading.astro'
import VideoRow from '~/src/components/video/row.astro'
const {
listing
} = Astro.props
const details = ensureListingDetails(listing)
---
{ details.hasRelatedVideos && (
<div
class="related-videos w-full"
>
<Heading text="Related Videos" />
<VideoRow
videos={details.relatedVideos}
/>
</div>
)}

View file

@ -0,0 +1,79 @@
---
import { ensureListingDetails } from '~/helpers/listing-page.js'
// import LinkButton from '~/components/link-button.js'
import Heading from './heading.astro'
const {
listing
} = Astro.props
const details = ensureListingDetails( listing )
const isNonNativeGame = listing.status !== 'native' && details.isGame
const links = [
{
label: '🔄 CrossOver Compatibility',
href: 'https://www.codeweavers.com/compatibility?ad=836'
},
{
label: '🔄 CrossOver Performance',
href: 'https://www.codeweavers.com/blog/jnewman/2020/11/23/more-crossover-m1-goodness-see-3-different-windows-games-running?ad=836'
},
{
label: '⏸ Parallels Compatibility',
href: 'https://prf.hn/l/pRelBQ5'
},
{
label: '⏸ Parallels Performance',
href: 'https://prf.hn/l/J9G0JeM'
}
]
const totalLinks = links.length
---
{ isNonNativeGame && (
<div
class="related-videos w-full"
>
<Heading text="Virtualization Support" />
<div class="text-xs opacity-75 mb-4">With Virtualization you can run apps on Apple Silicon Macs even if they are normally completely unsupported, such as Windows-only Apps, at the cost of some performance drop vs Native support. </div>
<span class="relative z-0 inline-flex text-center md:flex-row flex-col shadow-sm md:divide-x md:divide-y-0 divide-y divide-gray-700 border border-gray-300 rounded-md bg-darker md:py-3 md:px-0 px-4">
{ links.map( (link, i) => {
return (
<a
type="button"
href={ link.href }
class={ [
'relative inline-flex justify-center items-center font-medium focus:z-10 focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500',
'text-white group',
].join(' ')}
>
<div
class={[
'inner-link group-hover:bg-indigo-400 group-active:bg-indigo-600 rounded-md px-4 md:py-2 md:mx-0 md:-my-3',
'py-3 -mx-4',
// First Link
// i === 0 && 'rounded-l-md',
// Not first Link
i !== 0 ? 'md:-ml-px' : '',
// Last Link
// i === totalLinks - 1 ? 'rounded-r-md' : ''
].join(' ')}
>
{ link.label }
</div>
</a>
)
}
)}
</span>
</div>
)}

View file

@ -0,0 +1,35 @@
---
// Simple list for listing thinngs like categories, tags, etc.
const {
items
} = Astro.props
---
<ul class="simple-list space-y-3">
{ items.map( item => (
<li
class="relative"
>
<a
href={ item.href }
class={ [
'flex justify-start items-center inset-x-0 text-3xl md:text-4xl hover:bg-darkest focus:bg-gray-50 rounded-lg',
'border-2 border-white border-opacity-0 hover:border-opacity-50 focus:outline-none',
'duration-300 ease-in-out',
// Spacing
'space-x-3 -mx-5 px-5 md:pr-64 py-3'
].join(' ') }
style="transition-property: border;"
>
<div
class="font-hairline flex flex-col gap-3"
>
<h2>{ item.heading }</h2>
<div class="text-xs opacity-75 mb-3">{ item.description }</div>
</div>
<div>➔</div>
</a>
</li>
)) }
</ul>

View file

@ -0,0 +1,46 @@
---
// Stork Vanilla search
// Stork Eleventy Example - https://github.com/stork-search/netlify-11ty-example
import {
storkVersion,
// storkExecutableName,
// storkExecutablePath,
// storkTomlPath,
// storkIndexPath
} from '~/helpers/stork/config.js'
// const {
// listing
// } = Astro.props
const storkStylesheetURL = `https://files.stork-search.net/releases/v${ storkVersion }/basic.css`
const storkScriptURL = `https://files.stork-search.net/releases/v${ storkVersion }/stork.js`
---
<h1>Search</h1>
<link rel="stylesheet" href={ storkStylesheetURL } />
<div class="stork-wrapper">
<input data-stork="index" class="stork-input" />
<div data-stork="index-output" class="stork-output"></div>
</div>
<script is:inline src={ storkScriptURL }></script>
<script>
import { StorkClient } from '~/helpers/stork/browser.js'
const stork = new StorkClient({
name: 'index',
url: '/search-index.st',
// config: {}
stork: window.stork
})
const queryResult = await stork.lazyQuery( 'spo' )
console.log('queryResult', queryResult)
</script>

View file

@ -0,0 +1,79 @@
---
// Video Listing template for Benchmarks and Videos
import {
ensureListingDetails
} from '~/helpers/listing-page.js'
// import Devices from '~/src/components/listing-parts/devices.astro'
import RelatedVideos from '~/src/components/listing-parts/related-videos.astro'
import HtmlPlayer from '~/src/components/video/player.astro'
const {
listing
} = Astro.props
const details = ensureListingDetails( listing )
---
<section class="container pb-16">
<div class="flex flex-col items-center text-center space-y-6">
<HtmlPlayer
video={ details.initialVideo }
>
<div
slot="cover-bottom"
class="page-heading h-full flex items-end md:p-4"
>
<h1 class="title text-xs text-left md:text-2xl font-bold">{ details.mainHeading }</h1>
</div>
</HtmlPlayer>
<div class="md:flex w-full justify-between space-y-4 md:space-y-0 md:px-10">
{ details.shouldHaveSubscribeButton &&
<div
class="channel-credit"
>
<a
href={`https://www.youtube.com/channel/${ details.initialVideo.channel.id }?sub_confirmation=1`}
target="_blank"
rel="noopener"
role="button"
class="relative inline-flex items-center rounded-md px-4 py-2 leading-5 font-bold text-white border border-transparent focus:outline-none focus:border-indigo-600 neumorphic-shadow focus:shadow-outline-indigo bg-darker hover:bg-indigo-400 active:bg-indigo-600 transition duration-150 ease-in-out"
>Subscribe to { details.initialVideo.channel.name }</a>
</div>
}
</div>
<hr class="w-full">
{ details.hasRelatedApps &&
<div
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">
{ details.initialVideo.appLinks.map( appLink => (
<a
href={ appLink.endpoint }
role="button"
class="relative items-center leading-5 font-bold text-white border border-transparent focus:outline-none focus:border-indigo-600 neumorphic-shadow-inner bg-darker hover:bg-indigo-400 active:bg-indigo-600 transition duration-150 ease-in-out inline-block text-xs rounded-md px-4 py-2"
>{ appLink.name }</a>
) ) }
</div>
</div>
}
<RelatedVideos
listing={ listing }
/>
</div>
</section>

View file

@ -0,0 +1,33 @@
---
const {
video,
classes = ''
} = Astro.props
//
---
<div
class={[
'video-canvas w-screen flex justify-center inset-x-1/2 bg-black transition-opacity duration-500 ease-in-out',
classes
].join(' ')}
style="
margin-left: -50vw;
margin-right: -50vw;
-webkit-mask-image: linear-gradient(to top, rgba(0, 0, 0, 0) 25%, rgba(0, 0, 0, 0.25));
"
>
<div class="ratio-wrapper w-full">
<div class="relative overflow-hidden w-full pb-16/9">
<video
src={`https://vumbnail.com/${ video.id }.mp4`}
class="absolute w-full object-cover inset-0 blur-sm"
style="height: 200%;"
muted
autoplay
loop
playsinline
/>
</div>
</div>
</div>

View file

@ -0,0 +1,63 @@
---
import Poster from './poster.astro'
const {
video,
width = '325px',
classes = 'w-full flex-shrink-0 flex-grow-0 border-2 border-transparent rounded-2xl overflow-hidden'
} = Astro.props
const cardClasses = `video-card ${ classes }`
---
<div
class={ cardClasses }
style={ `max-width: ${ width }; flex-basis: ${ width }; scroll-snap-align: start;` }
>
<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">
<Poster video={video} />
</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>
{ ( video.tags.includes('benchmark') ) ?
<div
class="video-pill h-5 text-xs bg-white-2 flex justify-center items-center outline-0 rounded-full ease px-2"
>
Benchmark
</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>
<script>
import 'lazysizes'
</script>

View file

@ -0,0 +1,62 @@
---
import Poster from './poster.astro'
import Timestamps from './timestamps.astro'
const {
video,
width = '325px',
classes = 'w-full flex-shrink-0 flex-grow-0 border-2 border-transparent rounded-2xl overflow-hidden'
} = Astro.props
---
<lite-youtube
class="video-canvas w-screen flex flex-col justify-center items-center bg-black pt-16"
style="left:50%;right:50%;margin-left:-50vw;margin-right:-50vw;"
>
<script
class="video-data"
type="application/json"
set:html={ JSON.stringify( video ) }
/>
<div class="ratio-wrapper w-full max-w-4xl">
<div class="player-container relative overflow-hidden w-full pb-16/9">
<div class="player-poster cursor-pointer">
<Poster
video={ video }
/>
<div
class="video-card-overlay absolute inset-0 flex flex-col justify-center items-center bg-gradient-to-tr p-4"
style="
--tw-gradient-stops:
rgba(0, 0, 0, 1),
rgba(0, 0, 0, 0.6)
;
"
>
<div class="cover-top h-full"></div>
<div class="play-circle bg-white-2 bg-blur flex justify-center items-center outline-0 rounded-full ease p-4">
<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"></path>
</svg>
</div>
<div class="cover-bottom h-full">
<slot name="cover-bottom"/>
</div>
</div>
</div>
</div>
</div>
<Timestamps
video={ video }
/>
</lite-youtube>
<script>
import 'lazysizes'
import '~/helpers/lite-youtube.js'
</script>

View file

@ -0,0 +1,24 @@
---
import { getVideoImages } from '~/helpers/listing-page.js'
const {
video
} = Astro.props
const images = getVideoImages( video )
---
<picture>
{ Object.entries( images.sources ).map( ([ key, source ]) => (
<source
sizes={ source.sizes }
data-srcset={ source.srcset }
type={ `image/${ key }` }
>
) ) }
<img
data-src={ images.imgSrc }
alt={ video.name }
class="absolute inset-0 h-full w-full object-cover lazyload"
>
</picture>

View file

@ -0,0 +1,70 @@
---
import Card from './card.astro'
const {
videos,
cardWidth = '325',
classes = ''
} = Astro.props
// Math.random should be unique because of its seeding algorithm.
// Convert it to base 36 (numbers + letters), and grab the first 9 characters
// after the decimal.
const uid = Math.random().toString(36).substr(2, 9)
const rowId = `row-${ uid }`
const rowSelector = `#${ rowId }`
---
<div class="video-row relative w-full ${ classes }">
<div
id={ rowId }
class="video-row-contents flex overflow-x-auto whitespace-no-wrap py-2 space-x-6"
style="scroll-snap-type:x mandatory;"
>
{ videos.map(video => (
<Card
key={ video.id }
video={ video }
cardWidth={ cardWidth }
/>
)) }
</div>
<button
class="scroll-button 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"
style="top:50%;"
distance={ cardWidth * -1 }
scroll-target={ rowSelector }
aria-label="Scroll to previous videos"
>
<svg viewBox="0 0 20 20" fill="currentColor" class="h-5 w-5 text-gray-400" 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"></path>
</svg>
</button>
<button
class="scroll-button 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"
style="top:50%;"
distance={ cardWidth }
scroll-target={ rowSelector }
aria-label="Scroll to next videos"
>
<svg viewBox="0 0 20 20" fill="currentColor" class="h-5 w-5 text-gray-400">
<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"></path>
</svg>
</button>
</div>
<script>
import { scrollHorizontalCarousel } from '~/helpers/scroll.js'
// Add Click listeners to all buttons
Array.from( document.querySelectorAll(`.video-row button.scroll-button`) ).forEach( button => {
// console.log('button', button)
button.addEventListener('click', scrollHorizontalCarousel)
})
</script>

View file

@ -0,0 +1,26 @@
---
const {
video
} = Astro.props
const hasTimeStamps = video.timestamps.length > 0
---
{ hasTimeStamps &&
<div class="player-timestamps w-full max-w-4xl">
<div class="player-timestamps-wrapper md:inline-flex md:w-full max-w-4xl overflow-x-auto overflow-y-visible md:whitespace-nowrap rounded-xl py-3">
{ video.timestamps.map( timestamp => {
// const inSeconds = (minutes * 60) + Number(seconds)
return (
<button
time={ timestamp.time }
aria-label={`Jump to ${ timestamp.fullText }`}
class="inline-block text-xs rounded-lg border-2 border-white focus:outline-none border-opacity-0 neumorphic-shadow-inner px-3 py-2"
>
{ timestamp.fullText }
</button>
)
})}
</div>
</div>
}

44
src/layouts/default.astro Normal file
View file

@ -0,0 +1,44 @@
---
import '~/assets/css/tailwind.css'
import { PageHead } from '~/helpers/config-node.js'
import VueBaseLayout from '../../layouts/base.vue'
import GoogleAnalytics from '~/src/components/google-analytics.astro'
const {
// headTitle,
// headDescription,
headOptions = {}
} = Astro.props
// console.log('Astro.site', Astro.site )
const pageHead = new PageHead({
domain: Astro.site.origin,
...headOptions
})
---
<!doctype html>
<html lang="en">
<head>
<title>{ pageHead.title }</title>
<Fragment set:html={ pageHead.metaAndLinkMarkup } />
<GoogleAnalytics />
</head>
<body>
<VueBaseLayout
client:load
>
<slot />
</VueBaseLayout>
<!-- <script defer src="/_nuxt/static/1650919862/state.js"></script> -->
</body>
</html>

36
src/layouts/embed.astro Normal file
View file

@ -0,0 +1,36 @@
---
import '~/assets/css/tailwind.css'
import { PageHead } from '~/helpers/config-node.js'
import GoogleAnalytics from '~/src/components/google-analytics.astro'
const {
// headTitle,
// headDescription,
headOptions = {}
} = Astro.props
// console.log('Astro.site', Astro.site )
const pageHead = new PageHead({
domain: Astro.site.origin,
...headOptions
})
---
<!doctype html>
<html lang="en">
<head>
<title>{ pageHead.title }</title>
<Fragment set:html={ pageHead.metaAndLinkMarkup } />
<GoogleAnalytics />
</head>
<body>
<slot />
</body>
</html>

72
src/pages/[...page].astro Normal file
View file

@ -0,0 +1,72 @@
---
// import { getPathPartsFromAstroRequest } from '~/helpers/url.js'
import { catchRedirectResponse } from '~/helpers/astro/request.js'
import Layout from '../layouts/default.astro'
import LinkButton from '~/components/link-button.vue'
// Component Script:
// You can write any JavaScript/TypeScript that you'd like here.
// It will run during the build, but never in the browser.
// All variables are available to use in the HTML template below.
// Full Astro Component Syntax:
// https://docs.astro.build/core-concepts/astro-components/
const redirectResponse = await catchRedirectResponse( Astro )
if ( redirectResponse !== null ) {
return redirectResponse
}
// https://docs.astro.build/en/reference/api-reference/#astroresponse
Astro.response.status = 404
Astro.response.statusText = 'Not found'
---
<Layout
headOptions={ {
title: `Page is not compatible with Apple Silicon - Does It ARM`,
description: `Check for Apple Silicon compatibility for any of your apps instantly before you buy an ${ global.$config.processorsVerbiage } Mac. `,
// meta,
// link,
// structuredData: this.structuredData,
// domain,
// pathname: '/',
} }
>
<section class="container py-24">
<div class="flex flex-col items-center gap-8">
<h1 class="title text-3xl md:text-6xl font-hairline leading-tight text-center">
🤷‍♀️ Page is not compatible with Apple Silicon
</h1>
<h2 class="subtitle md:text-xl text-center">
Page not found
</h2>
<div class="flex flex-col items-center">
Perhaps the archives are incomplete, the page moved, or was deleted.
</div>
<LinkButton href="/">
Search
</LinkButton>
</div>
</section>
<!--
You can also use imported framework components directly in your markup!
Note: by default, these components are NOT interactive on the client.
The `:visible` directive tells Astro to make it interactive.
See https://docs.astro.build/core-concepts/component-hydration/
-->
</Layout>

View file

@ -0,0 +1,76 @@
---
// Full Astro Component Syntax:
// https://docs.astro.build/core-concepts/astro-components/
import { DoesItAPI } from '~/helpers/api/client.js'
import { catchRedirectResponse } from '~/helpers/astro/request.js'
import {
getVideoImages,
ListingDetails
} from '~/helpers/listing-page.js'
import { getPathPartsFromAstroRequest } from '~/helpers/url.js'
import Layout from '~/src/layouts/default.astro'
import Listing from '~/src/components/default-listing.astro'
import VideoListing from '~/src/components/video-listing.astro'
const redirectResponse = await catchRedirectResponse( Astro )
if ( redirectResponse !== null ) {
return redirectResponse
}
// Get type and slug from the request path
// so that we don't have extra parts for
// urls like /:type/:slug/benchmarks
const {
pathSlug,
subSlug = null
} = getPathPartsFromAstroRequest( Astro.request )
const isBenchmarkPage = subSlug === 'benchmarks'
// Astro Request reference
// https://docs.astro.build/en/reference/api-reference/#astrorequests
// Request App data from API
const appListing = await DoesItAPI.app( pathSlug ).get()
const listingDetails = new ListingDetails( appListing )
const headOptions = listingDetails.headOptions
if ( isBenchmarkPage ) {
// Set the page title
headOptions.title = `${ listingDetails.api.name } Benchmarks for Apple Silicon - Does It ARM`
const { preloads } = getVideoImages( listingDetails.initialVideo )
// Add image preloads for video thumbnail
headOptions.link = [
...headOptions.link,
...preloads
]
}
---
<Layout
headOptions={ headOptions }
>
{ isBenchmarkPage ? (
<VideoListing
listing={ appListing }
/>
) : (
<Listing
listing={ appListing }
/>
)}
</Layout>

View file

@ -0,0 +1,45 @@
---
import Layout from '../layouts/default.astro'
import AppTestPage from '~/pages/apple-silicon-app-test.vue'
// Component Script:
// You can write any JavaScript/TypeScript that you'd like here.
// It will run during the build, but never in the browser.
// All variables are available to use in the HTML template below.
// Full Astro Component Syntax:
// https://docs.astro.build/core-concepts/astro-components/
---
<Layout
headOptions={ {
title: `Apple Silicon Compatibility Test Online - Does It ARM`,
description: `Check for Apple Silicon compatibility for any of your apps instantly before you buy an ${ global.$config.processorsVerbiage } Mac. `,
// meta,
// link,
// structuredData: this.structuredData,
// domain,
pathname: '/',
} }
>
<AppTestPage
config={ global.$config }
client:load
/>
<!--
You can also use imported framework components directly in your markup!
Note: by default, these components are NOT interactive on the client.
The `:visible` directive tells Astro to make it interactive.
See https://docs.astro.build/core-concepts/component-hydration/
-->
</Layout>

217
src/pages/benchmarks.astro Normal file
View file

@ -0,0 +1,217 @@
---
// Component Script:
// You can write any JavaScript/TypeScript that you'd like here.
// It will run during the build, but never in the browser.
// All variables are available to use in the HTML template below.
// Full Astro Component Syntax:
// https://docs.astro.build/core-concepts/astro-components/
import { DoesItAPI } from '~/helpers/api/client.js'
import Layout from '../layouts/default.astro'
import BgPlayer from '~/src/components/video/bg-player.astro'
import VideoRow from '~/src/components/video/row.astro'
// import LinkButton from '~/components/link-button.vue'
const pagesToGet = 10
const allVideos = []
// Run through out newest video pages and get all the videos
await Promise.all(
Array.from({ length: pagesToGet }, (_, i) => i + 1).map(async (page) => {
// console.log('Getting page', page)
const videoPage = await DoesItAPI.kind.tv( page ).get()
// Merge in the new videos
allVideos.push( ...videoPage.items )
return videoPage
})
)
// Our initial video for the hero
const heroVideo = allVideos[0]
// Setup video rows data for the page
const 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: []
}
}
// Move videos to relevant categories one at a time
// so that we we don't get duplicates in the rows
for (const video of allVideos) {
// Look through row conditions to see if video matches
for (const rowKey in videoRows) {
if( videoRows[ rowKey ].matchesCondition(video) ) {
// Add the matching video
videoRows[ rowKey ].videos.push(video)
continue
}
}
}
---
<Layout
headOptions={ {
title: `Benchmarks for ${ global.$config.processorsVerbiage } Processors and Apple Silicon - Does It ARM`,
description: `Apple Silicon benchmark, performance, and compatibility videos for Macs using the ${ global.$config.processorsVerbiage } processors.`,
// meta,
// link,
// structuredData: this.structuredData,
// domain,
pathname: '/benchmarks',
} }
>
<main 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={ heroVideo }
classes="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={ heroVideo.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-bold">
{ heroVideo.name }
</h2>
</div>
</a>
<!-- <div
class="featured-apps-container 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"
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> -->
{ Object.entries(videoRows).map(([ rowKey, row ]) => {
// Skip rows that don't have enough videos
if ( row.videos.length < 3 ) return
return (
<div
class={ `${ rowKey }-videos w-full max-w-4xl` }
>
<h2 class="subtitle text-xl md:text-2xl font-bold mb-3">
{ row.heading }
</h2>
<VideoRow
client:load
videos={ row.videos }
/>
</div>
)
}) }
</div>
</main>
<!--
You can also use imported framework components directly in your markup!
Note: by default, these components are NOT interactive on the client.
The `:visible` directive tells Astro to make it interactive.
See https://docs.astro.build/core-concepts/component-hydration/
-->
</Layout>

View file

@ -0,0 +1,64 @@
---
// Component Script:
// You can write any JavaScript/TypeScript that you'd like here.
// It will run during the build, but never in the browser.
// All variables are available to use in the HTML template below.
// Full Astro Component Syntax:
// https://docs.astro.build/core-concepts/astro-components/
import { DoesItAPI } from '~/helpers/api/client.js'
import Layout from '../layouts/default.astro'
import SimpleList from '../components/simple-list.astro'
const kindIndex = await DoesItAPI.kind.index.get()
const kinds = Object.values( kindIndex ).map( category => {
return {
href: `/kind/${ category.kindName }`,
heading: category.label,
description: category.description,
}
})
---
<Layout
headOptions={ {
title: 'Categories of App Support lists for Apple Silicon',
description: `List of compatibility apps and games for Apple Silicon and the ${ this.$config.processorsVerbiage } Processors including performance reports and benchmarks`,
// meta,
// link,
// structuredData: this.structuredData,
// domain,
pathname: '/categories',
} }
>
<main class="container py-24">
<div class="flex flex-col">
<h1 class="title text-2xl leading-tight mb-6">
Categories
</h1>
<div class="line-separator border-white border-t-2 mb-12" />
<SimpleList
items={ kinds }
/>
</div>
</main>
<!--
You can also use imported framework components directly in your markup!
Note: by default, these components are NOT interactive on the client.
The `:visible` directive tells Astro to make it interactive.
See https://docs.astro.build/core-concepts/component-hydration/
-->
</Layout>

View file

@ -0,0 +1,119 @@
---
// Full Astro Component Syntax:
// https://docs.astro.build/core-concepts/astro-components/
import { DoesItAPI } from '~/helpers/api/client.js'
import { getPathPartsFromAstroRequest } from '~/helpers/url.js'
import { catchRedirectResponse } from '~/helpers/astro/request.js'
import { deviceSupportsApp } from '~/helpers/devices.js'
import Layout from '../../layouts/default.astro'
import Search from '~/components/search-stork.vue'
import LinkButton from '~/components/link-button.vue'
// Get type and slug from the request path
// so that we don't have extra parts for
// urls like /:type/:slug/benchmarks
const {
pathname,
pathSlug,
subSlug = 1
} = getPathPartsFromAstroRequest( Astro.request )
const redirectResponse = await catchRedirectResponse( Astro )
if ( redirectResponse !== null ) {
return redirectResponse
}
const device = await DoesItAPI.device( pathSlug ).get()
const rawAppPage = await DoesItAPI.kind( 'app' )( subSlug ).get()
const appPage = {
...rawAppPage,
// Map out paginnation links
// so we stay in the context of this device
previousPage: rawAppPage.previousPage.replace( '/kind/app', '/device/' + pathSlug ),
nextPage: rawAppPage.nextPage.replace( '/kind/app', '/device/' + pathSlug ),
// Map device support over text/status
items: rawAppPage.items.map( listing => {
const listingIsSupported = deviceSupportsApp( device, listing )
return {
...listing,
text: listingIsSupported ? `✅ Supported on ${ device.name }` : `🚫 Not yet reported working on ${ device.name }`,
}
})
}
---
<Layout
headOptions={{
title: `Apple Silicon Support for ${ device.name }`,
description: `Check reported Apple Silicon Support status of apps on ${ device.name }. `,
// meta,
// link,
// structuredData: this.structuredData,
// domain,
pathname
}}
>
<section class="container py-24">
<div class="flex flex-col items-center space-y-4">
<div class="hero">
<h1 class="title text-3xl md:text-5xl font-hairline leading-tight text-center pb-4">
Apple Silicon Support for { device.name }
</h1>
<div
class="subtitle md:text-xl text-center"
>
{ device.description }
</div>
<br>
<div
class="subtitle md:text-xl text-center"
>
Supported apps include { appPage.summary.sampleNamesShort }.
</div>
</div>
{ !!device.amazonUrl &&
<div class="flex justify-center py-8">
<LinkButton
href={ device.amazonUrl }
target="_blank"
>
Check Pricing
</LinkButton>
</div>
}
<Search
kind-page={ appPage }
client:load
/>
<!-- <ListEndButtons query="query" /> -->
<!-- <AllUpdatesSubscribe
class="my-12"
/> -->
</div>
</section>
</Layout>

65
src/pages/devices.astro Normal file
View file

@ -0,0 +1,65 @@
---
// Component Script:
// You can write any JavaScript/TypeScript that you'd like here.
// It will run during the build, but never in the browser.
// All variables are available to use in the HTML template below.
// Full Astro Component Syntax:
// https://docs.astro.build/core-concepts/astro-components/
import { DoesItAPI } from '~/helpers/api/client.js'
import Layout from '../layouts/default.astro'
import SimpleList from '../components/simple-list.astro'
const deviceIndex = await DoesItAPI.kind.device(1).get()
const kinds = deviceIndex.items.map( device => {
return {
href: device.endpoint,
heading: device.name,
description: device.description,
}
})
---
<Layout
headOptions={ {
title: 'List of Apple Devices for Apple Silicon App Support',
description: `List of devices for Apple Silicon and the ${ global.$config.processorsVerbiage } Processors`,
// meta,
// link,
// structuredData: this.structuredData,
// domain,
pathname: '/devices',
} }
>
<main class="container py-24">
<div class="flex flex-col">
<h1 class="title text-2xl leading-tight mb-6">
Categories
</h1>
<div class="line-separator border-white border-t-2 mb-12" />
<SimpleList
items={ kinds }
/>
</div>
</main>
<!--
You can also use imported framework components directly in your markup!
Note: by default, these components are NOT interactive on the client.
The `:visible` directive tells Astro to make it interactive.
See https://docs.astro.build/core-concepts/component-hydration/
-->
</Layout>

View file

@ -0,0 +1,92 @@
---
import { getPathPartsFromAstroRequest } from '~/helpers/url.js'
import Layout from '~/src/layouts/embed.astro'
import VideoPlayer from '~/components/video/player.vue'
import '@fontsource/inter/variable.css'
// Component Script:
// You can write any JavaScript/TypeScript that you'd like here.
// It will run during the build, but never in the browser.
// All variables are available to use in the HTML template below.
// Full Astro Component Syntax:
// https://docs.astro.build/core-concepts/astro-components/
// Get type and slug from the request path
// so that we don't have extra parts for
// urls like /:type/:slug/benchmarks
const {
pathSlug,
subSlug = null,
params
} = getPathPartsFromAstroRequest( Astro.request )
const {
name,
'youtube-id' : youtubeId
} = params
const video = {
name,
id: youtubeId,
timestamps: [],
thumbnail: {
sizes: '(max-width: 640px) 100vw, 640px',
srcset: `https://i.ytimg.com/vi/${ youtubeId }/default.jpg 120w, https://i.ytimg.com/vi/${ youtubeId }/mqdefault.jpg 320w, https://i.ytimg.com/vi/${ youtubeId }/hqdefault.jpg 480w, https://i.ytimg.com/vi/${ youtubeId }/sddefault.jpg 640w`,
src: `https://i.ytimg.com/vi/${ youtubeId }/default.jpg`
},
}
---
<Layout
headOptions={ {
title: 'Video - Does It ARM',
// description: ``,
// meta,
// link,
// structuredData: this.structuredData,
// domain,
pathname: '/',
} }
>
<style>
/* Clear out background color */
html {
background: transparent;
}
</style>
<div class="embed-main text-gray-300">
<VideoPlayer
client:load
video={ video }
class="w-100 h-100 absolute inset-0 flex justify-center items-center"
>
<div class="page-heading h-full flex items-end md:p-4">
<h1 class="title text-xs text-left md:text-2xl font-bold">
{ video.name }
</h1>
</div>
</VideoPlayer>
</div>
<!--
You can also use imported framework components directly in your markup!
Note: by default, these components are NOT interactive on the client.
The `:visible` directive tells Astro to make it interactive.
See https://docs.astro.build/core-concepts/component-hydration/
-->
</Layout>

View file

@ -0,0 +1,49 @@
---
// Full Astro Component Syntax:
// https://docs.astro.build/core-concepts/astro-components/
import { DoesItAPI } from '~/helpers/api/client.js'
import { catchRedirectResponse } from '~/helpers/astro/request.js'
import {
ListingDetails
} from '~/helpers/listing-page.js'
import { getPathPartsFromAstroRequest } from '~/helpers/url.js'
import Layout from '~/src/layouts/default.astro'
import Listing from '~/src/components/default-listing.astro'
const redirectResponse = await catchRedirectResponse( Astro )
if ( redirectResponse !== null ) {
return redirectResponse
}
// Get type and slug from the request path
// so that we don't have extra parts for
// urls like /:type/:slug/benchmarks
const {
pathSlug,
subSlug = null
} = getPathPartsFromAstroRequest( Astro.request )
// Astro Request reference
// https://docs.astro.build/en/reference/api-reference/#astrorequests
// Request App data from API
const appListing = await DoesItAPI.formula( pathSlug ).get()
const listingDetails = new ListingDetails( appListing )
const headOptions = listingDetails.headOptions
---
<Layout
headOptions={ headOptions }
>
<Listing
listing={ appListing }
/>
</Layout>

View file

@ -0,0 +1,76 @@
---
// Full Astro Component Syntax:
// https://docs.astro.build/core-concepts/astro-components/
import { DoesItAPI } from '~/helpers/api/client.js'
import { catchRedirectResponse } from '~/helpers/astro/request.js'
import {
getVideoImages,
ListingDetails
} from '~/helpers/listing-page.js'
import { getPathPartsFromAstroRequest } from '~/helpers/url.js'
import Layout from '~/src/layouts/default.astro'
import Listing from '~/src/components/default-listing.astro'
import VideoListing from '~/src/components/video-listing.astro'
const redirectResponse = await catchRedirectResponse( Astro )
if ( redirectResponse !== null ) {
return redirectResponse
}
// Get type and slug from the request path
// so that we don't have extra parts for
// urls like /:type/:slug/benchmarks
const {
pathSlug,
subSlug = null
} = getPathPartsFromAstroRequest( Astro.request )
const isBenchmarkPage = subSlug === 'benchmarks'
// Astro Request reference
// https://docs.astro.build/en/reference/api-reference/#astrorequests
// Request App data from API
const appListing = await DoesItAPI.game( pathSlug ).get()
const listingDetails = new ListingDetails( appListing )
const headOptions = listingDetails.headOptions
if ( isBenchmarkPage ) {
// Set the page title
headOptions.title = `${ listingDetails.api.name } Benchmarks for Apple Silicon - Does It ARM`
const { preloads } = getVideoImages( listingDetails.initialVideo )
// Add image preloads for video thumbnail
headOptions.link = [
...headOptions.link,
...preloads
]
}
---
<Layout
headOptions={ headOptions }
>
{ isBenchmarkPage ? (
<VideoListing
listing={ appListing }
/>
) : (
<Listing
listing={ appListing }
/>
)}
</Layout>

104
src/pages/games.astro Normal file
View file

@ -0,0 +1,104 @@
---
// Full Astro Component Syntax:
// https://docs.astro.build/core-concepts/astro-components/
import { DoesItAPI } from '~/helpers/api/client.js'
import { getPathPartsFromAstroRequest } from '~/helpers/url.js'
import { catchRedirectResponse } from '~/helpers/astro/request.js'
import {
categories,
makeCategoryFilterFromCategorySlug
} from '~/helpers/categories.js'
import Layout from '~/src/layouts/default.astro'
import Search from '~/components/search-stork.vue'
import ThomasCredit from '~/components/thomas-credit.vue'
// Get type and slug from the request path
// so that we don't have extra parts for
// urls like /:type/:slug/benchmarks
const {
pathname,
// pathSlug,
// subSlug = 1
} = getPathPartsFromAstroRequest( Astro.request )
const redirectResponse = await catchRedirectResponse( Astro )
if ( redirectResponse !== null ) {
return redirectResponse
}
const rawKindPage = await DoesItAPI.kind( 'game' )( 1 ).get()
// Clean up unused kind data
const kindPage = {
...rawKindPage,
items: rawKindPage.items.map( item => ({
...item,
bundles: undefined,
}) )
}
const baseFilters = []
const categorySlug = 'games'
const category = categories[ categorySlug ]
baseFilters.push( makeCategoryFilterFromCategorySlug( categorySlug ) )
const pageLabel = category?.pluralLabel || category.label
---
<Layout
headOptions={{
title: `List of ${ pageLabel } that work on Apple Silicon?`,
description: `Check the latest reported support status of ${ pageLabel } on Apple Silicon and ${ global.$config.processorsVerbiage } Processors. `,
// meta,
// link,
// structuredData: this.structuredData,
// domain,
pathname
}}
>
<section class="container py-24">
<div class="flex flex-col items-center space-y-4">
<div class="hero">
<h1 class="title text-3xl md:text-5xl font-hairline leading-tight text-center pb-4">
{ pageLabel } that are reported to support Apple Silicon
</h1>
<h2
v-if="supportedAppList.length !== 0"
class="subtitle md:text-xl text-center"
>
Supported apps include { kindPage.summary.sampleNamesShort }.
</h2>
<ThomasCredit />
</div>
<Search
kind-page={ kindPage }
base-filters={ baseFilters }
client:load
/>
<!-- <ListEndButtons query="query" /> -->
<!-- <AllUpdatesSubscribe
class="my-12"
/> -->
</div>
</section>
</Layout>

72
src/pages/index.astro Normal file
View file

@ -0,0 +1,72 @@
---
// Component Script:
// You can write any JavaScript/TypeScript that you'd like here.
// It will run during the build, but never in the browser.
// All variables are available to use in the HTML template below.
// Full Astro Component Syntax:
// https://docs.astro.build/core-concepts/astro-components/
import { DoesItAPI } from '~/helpers/api/client.js'
import Layout from '../layouts/default.astro'
import Search from '~/components/search-stork.vue'
// import ListSummary from '~/components/list-summary.vue'
import ListEndButtons from '~/components/list-end-buttons.vue'
import AllUpdatesSubscribe from '~/components/all-updates-subscribe.vue'
const homePageKindPage = await DoesItAPI.kind.app(1).get()
const allAppsSummary = await DoesItAPI('all-apps-summary').get()
// console.log( allAppsSummary )
---
<Layout
headOptions={ {
title: `Apple Silicon and ${ global.$config.processorsVerbiage } app and game compatibility list`,
description: `List of compatibility apps and games for Apple Silicon and the ${ global.$config.processorsVerbiage } Processors including performance reports and benchmarks`,
// meta,
// link,
// structuredData: this.structuredData,
// domain,
pathname: '/',
} }
>
<section class="container py-24">
<div class="flex flex-col items-center space-y-4">
<div class="hero">
<h1 class="title text-3xl md:text-6xl font-hairline leading-tight text-center">
Does It ARM?
</h1>
<h2 class="subtitle md:text-xl text-center">
Apps that are reported to support Apple Silicon
</h2>
</div>
<Search
kind-page={ homePageKindPage }
list-summary={ allAppsSummary }
client:load
/>
<AllUpdatesSubscribe
class="my-12"
/>
</div>
</section>
<!--
You can also use imported framework components directly in your markup!
Note: by default, these components are NOT interactive on the client.
The `:visible` directive tells Astro to make it interactive.
See https://docs.astro.build/core-concepts/component-hydration/
-->
</Layout>

View file

@ -0,0 +1,112 @@
---
// Full Astro Component Syntax:
// https://docs.astro.build/core-concepts/astro-components/
import { DoesItAPI } from '~/helpers/api/client.js'
import { getPathPartsFromAstroRequest } from '~/helpers/url.js'
import { catchRedirectResponse } from '~/helpers/astro/request.js'
import {
categories,
getKindToCategorySlug,
getCategoryKindName,
makeCategoryFilterFromCategorySlug
} from '~/helpers/categories.js'
import Layout from '../../layouts/default.astro'
import Search from '~/components/search-stork.vue'
// Get type and slug from the request path
// so that we don't have extra parts for
// urls like /:type/:slug/benchmarks
const {
pathname,
pathSlug,
subSlug = 1
} = getPathPartsFromAstroRequest( Astro.request )
const redirectResponse = await catchRedirectResponse( Astro )
if ( redirectResponse !== null ) {
return redirectResponse
}
// Try the pathSlug against categories
// so we can load from category slugs
const kindName = getCategoryKindName( pathSlug ) ? getCategoryKindName( pathSlug ) : pathSlug
const rawKindPage = await DoesItAPI.kind( kindName )( subSlug ).get()
// Clean up unused kind data
const kindPage = {
...rawKindPage,
items: rawKindPage.items.map( item => ({
...item,
bundles: undefined,
}) )
}
let pageLabel = 'Mac Apps'
const baseFilters = []
const categorySlug = getKindToCategorySlug( kindName )
// If we have a category slug, add a filter
if ( !!categorySlug ) {
const category = categories[ categorySlug ]
baseFilters.push( makeCategoryFilterFromCategorySlug( categorySlug ) )
pageLabel = category?.pluralLabel || category.label
}
---
<Layout
headOptions={{
title: `List of ${ pageLabel } that work on Apple Silicon?`,
description: `Check the latest reported support status of ${ pageLabel } on Apple Silicon and ${ global.$config.processorsVerbiage } Processors. `,
// meta,
// link,
// structuredData: this.structuredData,
// domain,
pathname
}}
>
<section class="container py-24">
<div class="flex flex-col items-center space-y-4">
<div class="hero">
<h1 class="title text-3xl md:text-5xl font-hairline leading-tight text-center pb-4">
{ pageLabel } that are reported to support Apple Silicon
</h1>
<h2
v-if="supportedAppList.length !== 0"
class="subtitle md:text-xl text-center"
>
Supported apps include { kindPage.summary.sampleNamesShort }.
</h2>
</div>
<Search
kind-page={ kindPage }
base-filters={ baseFilters }
client:load
/>
<!-- <ListEndButtons query="query" /> -->
<!-- <AllUpdatesSubscribe
class="my-12"
/> -->
</div>
</section>
</Layout>

Some files were not shown because too many files have changed in this diff Show more