stable build
101
android/.gitignore
vendored
Normal file
@@ -0,0 +1,101 @@
|
||||
# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore
|
||||
|
||||
# Built application files
|
||||
*.apk
|
||||
*.aar
|
||||
*.ap_
|
||||
*.aab
|
||||
|
||||
# Files for the ART/Dalvik VM
|
||||
*.dex
|
||||
|
||||
# Java class files
|
||||
*.class
|
||||
|
||||
# Generated files
|
||||
bin/
|
||||
gen/
|
||||
out/
|
||||
# Uncomment the following line in case you need and you don't have the release build type files in your app
|
||||
# release/
|
||||
|
||||
# Gradle files
|
||||
.gradle/
|
||||
build/
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
local.properties
|
||||
|
||||
# Proguard folder generated by Eclipse
|
||||
proguard/
|
||||
|
||||
# Log Files
|
||||
*.log
|
||||
|
||||
# Android Studio Navigation editor temp files
|
||||
.navigation/
|
||||
|
||||
# Android Studio captures folder
|
||||
captures/
|
||||
|
||||
# IntelliJ
|
||||
*.iml
|
||||
.idea/workspace.xml
|
||||
.idea/tasks.xml
|
||||
.idea/gradle.xml
|
||||
.idea/assetWizardSettings.xml
|
||||
.idea/dictionaries
|
||||
.idea/libraries
|
||||
# Android Studio 3 in .gitignore file.
|
||||
.idea/caches
|
||||
.idea/modules.xml
|
||||
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
|
||||
.idea/navEditor.xml
|
||||
|
||||
# Keystore files
|
||||
# Uncomment the following lines if you do not want to check your keystore files in.
|
||||
#*.jks
|
||||
#*.keystore
|
||||
|
||||
# External native build folder generated in Android Studio 2.2 and later
|
||||
.externalNativeBuild
|
||||
.cxx/
|
||||
|
||||
# Google Services (e.g. APIs or Firebase)
|
||||
# google-services.json
|
||||
|
||||
# Freeline
|
||||
freeline.py
|
||||
freeline/
|
||||
freeline_project_description.json
|
||||
|
||||
# fastlane
|
||||
fastlane/report.xml
|
||||
fastlane/Preview.html
|
||||
fastlane/screenshots
|
||||
fastlane/test_output
|
||||
fastlane/readme.md
|
||||
|
||||
# Version control
|
||||
vcs.xml
|
||||
|
||||
# lint
|
||||
lint/intermediates/
|
||||
lint/generated/
|
||||
lint/outputs/
|
||||
lint/tmp/
|
||||
# lint/reports/
|
||||
|
||||
# Android Profiling
|
||||
*.hprof
|
||||
|
||||
# Cordova plugins for Capacitor
|
||||
capacitor-cordova-android-plugins
|
||||
|
||||
# Copied web assets
|
||||
app/src/main/assets/public
|
||||
|
||||
# Generated Config files
|
||||
app/src/main/assets/capacitor.config.json
|
||||
app/src/main/assets/capacitor.plugins.json
|
||||
app/src/main/res/xml/config.xml
|
||||
2
android/app/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/build/*
|
||||
!/build/.npmkeep
|
||||
54
android/app/build.gradle
Normal file
@@ -0,0 +1,54 @@
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
namespace "tv.vyla.app"
|
||||
compileSdk rootProject.ext.compileSdkVersion
|
||||
defaultConfig {
|
||||
applicationId "tv.vyla.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
|
||||
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
flatDir{
|
||||
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
||||
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
||||
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
|
||||
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
|
||||
implementation project(':capacitor-android')
|
||||
testImplementation "junit:junit:$junitVersion"
|
||||
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
||||
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
||||
implementation project(':capacitor-cordova-android-plugins')
|
||||
}
|
||||
|
||||
apply from: 'capacitor.build.gradle'
|
||||
|
||||
try {
|
||||
def servicesJSON = file('google-services.json')
|
||||
if (servicesJSON.text) {
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
}
|
||||
} catch(Exception e) {
|
||||
logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
|
||||
}
|
||||
19
android/app/capacitor.build.gradle
Normal file
@@ -0,0 +1,19 @@
|
||||
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
|
||||
|
||||
android {
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
}
|
||||
|
||||
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||
dependencies {
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
if (hasProperty('postBuildExtras')) {
|
||||
postBuildExtras()
|
||||
}
|
||||
21
android/app/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.getcapacitor.myapp;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class ExampleInstrumentedTest {
|
||||
|
||||
@Test
|
||||
public void useAppContext() throws Exception {
|
||||
// Context of the app under test.
|
||||
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
|
||||
|
||||
assertEquals("com.getcapacitor.app", appContext.getPackageName());
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
||||
|
||||
<application
|
||||
@@ -9,8 +10,7 @@
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme"
|
||||
android:banner="@mipmap/ic_launcher">
|
||||
android:theme="@style/AppTheme">
|
||||
|
||||
<activity
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
|
||||
@@ -27,5 +27,19 @@
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths"></meta-data>
|
||||
</provider>
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
<!-- Permissions -->
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
</manifest>
|
||||
|
||||
5
android/app/src/main/java/tv/vyla/app/MainActivity.java
Normal file
@@ -0,0 +1,5 @@
|
||||
package tv.vyla.app;
|
||||
|
||||
import com.getcapacitor.BridgeActivity;
|
||||
|
||||
public class MainActivity extends BridgeActivity {}
|
||||
BIN
android/app/src/main/res/drawable-land-hdpi/splash.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
android/app/src/main/res/drawable-land-mdpi/splash.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
android/app/src/main/res/drawable-land-xhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
android/app/src/main/res/drawable-land-xxhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
android/app/src/main/res/drawable-land-xxxhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
android/app/src/main/res/drawable-port-hdpi/splash.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
android/app/src/main/res/drawable-port-mdpi/splash.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
android/app/src/main/res/drawable-port-xhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
BIN
android/app/src/main/res/drawable-port-xxhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
android/app/src/main/res/drawable-port-xxxhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,34 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportHeight="108"
|
||||
android:viewportWidth="108">
|
||||
<path
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeWidth="1">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="78.5885"
|
||||
android:endY="90.9159"
|
||||
android:startX="48.7653"
|
||||
android:startY="61.0927"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeWidth="1" />
|
||||
</vector>
|
||||
170
android/app/src/main/res/drawable/ic_launcher_background.xml
Normal file
@@ -0,0 +1,170 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportHeight="108"
|
||||
android:viewportWidth="108">
|
||||
<path
|
||||
android:fillColor="#26A69A"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
</vector>
|
||||
BIN
android/app/src/main/res/drawable/splash.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
12
android/app/src/main/res/layout/activity_main.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<WebView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 15 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#FFFFFF</color>
|
||||
</resources>
|
||||
7
android/app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<resources>
|
||||
<string name="app_name">Vyla</string>
|
||||
<string name="title_activity_main">Vyla</string>
|
||||
<string name="package_name">tv.vyla.app</string>
|
||||
<string name="custom_url_scheme">tv.vyla.app</string>
|
||||
</resources>
|
||||
22
android/app/src/main/res/values/styles.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<!-- Base application theme. -->
|
||||
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
<item name="colorAccent">@color/colorAccent</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||
<item name="windowActionBar">false</item>
|
||||
<item name="windowNoTitle">true</item>
|
||||
<item name="android:background">@null</item>
|
||||
</style>
|
||||
|
||||
|
||||
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
|
||||
<item name="android:background">@drawable/splash</item>
|
||||
</style>
|
||||
</resources>
|
||||
5
android/app/src/main/res/xml/file_paths.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<external-path name="my_images" path="." />
|
||||
<cache-path name="my_cache_images" path="." />
|
||||
</paths>
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.getcapacitor.myapp;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||
*/
|
||||
public class ExampleUnitTest {
|
||||
|
||||
@Test
|
||||
public void addition_isCorrect() throws Exception {
|
||||
assertEquals(4, 2 + 2);
|
||||
}
|
||||
}
|
||||
29
android/build.gradle
Normal file
@@ -0,0 +1,29 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.2.1'
|
||||
classpath 'com.google.gms:google-services:4.4.0'
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
}
|
||||
}
|
||||
|
||||
apply from: "variables.gradle"
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
3
android/capacitor.settings.gradle
Normal file
@@ -0,0 +1,3 @@
|
||||
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
|
||||
include ':capacitor-android'
|
||||
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
|
||||
22
android/gradle.properties
Normal file
@@ -0,0 +1,22 @@
|
||||
# Project-wide Gradle settings.
|
||||
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx1536m
|
||||
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
BIN
android/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
7
android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-all.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
248
android/gradlew
vendored
Normal file
@@ -0,0 +1,248 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
92
android/gradlew.bat
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
5
android/settings.gradle
Normal file
@@ -0,0 +1,5 @@
|
||||
include ':app'
|
||||
include ':capacitor-cordova-android-plugins'
|
||||
project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')
|
||||
|
||||
apply from: 'capacitor.settings.gradle'
|
||||
16
android/variables.gradle
Normal file
@@ -0,0 +1,16 @@
|
||||
ext {
|
||||
minSdkVersion = 22
|
||||
compileSdkVersion = 34
|
||||
targetSdkVersion = 34
|
||||
androidxActivityVersion = '1.8.0'
|
||||
androidxAppCompatVersion = '1.6.1'
|
||||
androidxCoordinatorLayoutVersion = '1.2.0'
|
||||
androidxCoreVersion = '1.12.0'
|
||||
androidxFragmentVersion = '1.6.2'
|
||||
coreSplashScreenVersion = '1.0.1'
|
||||
androidxWebkitVersion = '1.9.0'
|
||||
junitVersion = '4.13.2'
|
||||
androidxJunitVersion = '1.1.5'
|
||||
androidxEspressoCoreVersion = '3.5.1'
|
||||
cordovaAndroidVersion = '10.1.1'
|
||||
}
|
||||
1993
package-lock.json
generated
11
package.json
@@ -6,14 +6,13 @@
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"build:web": "node -e \"const fs = require('fs'); fs.rmSync('www', { recursive: true, force: true }); fs.mkdirSync('www'); ['css', 'js', 'assets'].forEach(d => fs.cpSync(d, 'www/' + d, { recursive: true })); fs.copyFileSync('index.html', 'www/index.html');\"",
|
||||
"cap:sync": "npm run build:web && cap sync android",
|
||||
"dev": "npm run build:web && cap sync android && cd android && gradlew.bat assembleDebug",
|
||||
"release": "npm run build:web && cap sync android && cd android && gradlew.bat assembleRelease"
|
||||
"dev": "npm run build:web && cap sync android && cd android && gradlew.bat assembleDebug && cd .. && node -e \"require('fs').copyFileSync('android/app/build/outputs/apk/debug/app-debug.apk', 'Vyla-TV-debug.apk')\"",
|
||||
"release": "npm run build:web && cap sync android && cd android && gradlew.bat assembleRelease && cd .. && node -e \"require('fs').copyFileSync('android/app/build/outputs/apk/release/app-release-unsigned.apk', 'Vyla-TV-release.apk')\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@capacitor/android": "^6.0.0",
|
||||
"@capacitor/cli": "^6.0.0",
|
||||
"@capacitor/core": "^6.0.0",
|
||||
"@capacitor/android": "^6.2.2",
|
||||
"@capacitor/cli": "^6.2.2",
|
||||
"@capacitor/core": "^6.2.2",
|
||||
"@vyla-entertainment/sdk": "^1.14.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.19.2"
|
||||
|
||||
BIN
www/assets/favicon.ico
Normal file
|
After Width: | Height: | Size: 27 KiB |
315
www/css/base.css
Normal file
@@ -0,0 +1,315 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
outline: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
:root {
|
||||
--pad: clamp(56px, 6vw, 132px);
|
||||
--cw: clamp(120px, 9.4vw, 200px);
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
background: #07070a;
|
||||
color: #f2f2f5;
|
||||
font-family: "Inter", "SF Pro Display", "Segoe UI", Roboto, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
overflow: hidden;
|
||||
cursor: default
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
display: none
|
||||
}
|
||||
|
||||
.f {
|
||||
outline: none;
|
||||
cursor: pointer
|
||||
}
|
||||
|
||||
.xf {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-size: cover;
|
||||
background-position: center top;
|
||||
opacity: 0;
|
||||
transition: opacity .7s ease
|
||||
}
|
||||
|
||||
.xf.in {
|
||||
opacity: 1
|
||||
}
|
||||
|
||||
#bg {
|
||||
position: fixed;
|
||||
inset: -40px;
|
||||
background: #07070a;
|
||||
filter: blur(42px) brightness(.55) saturate(1)
|
||||
}
|
||||
|
||||
#dim {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: linear-gradient(180deg, rgba(0, 0, 0, .05) 0%, rgba(0, 0, 0, .55) 55%, #000 100%)
|
||||
}
|
||||
|
||||
#dim2 {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: #000;
|
||||
opacity: 0;
|
||||
pointer-events: none
|
||||
}
|
||||
|
||||
#main {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
scrollbar-width: none
|
||||
}
|
||||
|
||||
.hero {
|
||||
position: relative;
|
||||
height: 60vh
|
||||
}
|
||||
|
||||
#hb {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
-webkit-mask-image: linear-gradient(180deg, #000 50%, transparent 100%);
|
||||
mask-image: linear-gradient(180deg, #000 50%, transparent 100%)
|
||||
}
|
||||
|
||||
#hb::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(90deg, rgba(0, 0, 0, .8) 0%, rgba(0, 0, 0, .35) 40%, transparent 70%)
|
||||
}
|
||||
|
||||
.hc {
|
||||
position: absolute;
|
||||
left: var(--pad);
|
||||
bottom: 5vh;
|
||||
width: min(48vw, 680px);
|
||||
z-index: 2
|
||||
}
|
||||
|
||||
#hlogo {
|
||||
height: 16vh;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
margin-bottom: 2vh
|
||||
}
|
||||
|
||||
#hlogo img {
|
||||
max-width: min(34vw, 440px);
|
||||
max-height: 16vh;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 4px 18px rgba(0, 0, 0, .6))
|
||||
}
|
||||
|
||||
#hlogo h1 {
|
||||
font-size: clamp(28px, 3.6vw, 64px);
|
||||
font-weight: 700;
|
||||
letter-spacing: -.02em;
|
||||
line-height: 1.02;
|
||||
text-shadow: 0 4px 24px rgba(0, 0, 0, .6)
|
||||
}
|
||||
|
||||
.meta {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
align-items: center;
|
||||
font-size: clamp(13px, 1.05vw, 19px);
|
||||
color: #e3e3ee;
|
||||
margin-bottom: 1.4vh;
|
||||
font-weight: 500
|
||||
}
|
||||
|
||||
.meta .b {
|
||||
background: rgba(255, 255, 255, .18);
|
||||
border-radius: 6px;
|
||||
padding: 2px 8px;
|
||||
font-size: .85em;
|
||||
font-weight: 600
|
||||
}
|
||||
|
||||
.ov1 {
|
||||
font-size: clamp(13px, 1.1vw, 20px);
|
||||
line-height: 1.5;
|
||||
color: #d4d4e2;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
margin-bottom: 2.4vh;
|
||||
max-width: 640px
|
||||
}
|
||||
|
||||
.hb {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
align-items: center
|
||||
}
|
||||
|
||||
.pb,
|
||||
.ib {
|
||||
border: 0;
|
||||
font: inherit;
|
||||
font-weight: 600;
|
||||
font-size: clamp(14px, 1.1vw, 20px);
|
||||
border-radius: 999px;
|
||||
height: clamp(44px, 3.6vw, 64px);
|
||||
padding: 0 clamp(28px, 2.6vw, 48px);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
transition: transform .2s, background .2s
|
||||
}
|
||||
|
||||
.pb {
|
||||
background: #fff;
|
||||
color: #000
|
||||
}
|
||||
|
||||
.ib {
|
||||
background: rgba(255, 255, 255, .16);
|
||||
color: #fff;
|
||||
backdrop-filter: blur(12px);
|
||||
padding: 0 clamp(20px, 2vw, 36px)
|
||||
}
|
||||
|
||||
.pb.on,
|
||||
.ib.on {
|
||||
transform: scale(1.04);
|
||||
}
|
||||
|
||||
.ib.on {
|
||||
background: rgba(255, 255, 255, .3)
|
||||
}
|
||||
|
||||
.sec {
|
||||
padding-top: 10px
|
||||
}
|
||||
|
||||
.sec h2 {
|
||||
padding: 0 var(--pad);
|
||||
font-size: clamp(16px, 1.5vw, 26px);
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px;
|
||||
text-shadow: 0 2px 10px rgba(0, 0, 0, .6)
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
padding: 22px var(--pad) 26px;
|
||||
position: relative;
|
||||
scrollbar-width: none;
|
||||
min-height: calc(var(--cw)*1.5 + 48px)
|
||||
}
|
||||
|
||||
.card {
|
||||
flex: 0 0 var(--cw);
|
||||
height: calc(var(--cw)*1.5);
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
background: #1b1b24;
|
||||
position: relative;
|
||||
transform: translateZ(0);
|
||||
transition: transform .38s cubic-bezier(.22, .9, .3, 1);
|
||||
will-change: transform
|
||||
}
|
||||
|
||||
.card img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
filter: brightness(.86);
|
||||
transition: filter .38s
|
||||
}
|
||||
|
||||
.card::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 3;
|
||||
border-radius: inherit;
|
||||
opacity: 0;
|
||||
transition: opacity .25s;
|
||||
pointer-events: none
|
||||
}
|
||||
|
||||
.card.on {
|
||||
transform: scale(1.06) translateY(-4px);
|
||||
z-index: 2
|
||||
}
|
||||
|
||||
.card.on img {
|
||||
filter: none
|
||||
}
|
||||
|
||||
.card.on::after {
|
||||
opacity: 1
|
||||
}
|
||||
|
||||
.kbm,
|
||||
.kbm * {
|
||||
cursor: none !important
|
||||
}
|
||||
|
||||
#sentinel {
|
||||
height: 40vh
|
||||
}
|
||||
|
||||
.none {
|
||||
color: rgba(255, 255, 255, .6);
|
||||
padding: 5vh 0;
|
||||
font-size: 18px
|
||||
}
|
||||
|
||||
|
||||
.toast {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
bottom: 7vh;
|
||||
transform: translateX(-50%);
|
||||
z-index: 200;
|
||||
background: #fff;
|
||||
color: #000;
|
||||
padding: 12px 22px;
|
||||
border-radius: 999px;
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
animation: tin .25s both, tout .3s 1.9s forwards
|
||||
}
|
||||
|
||||
@keyframes tin {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, 14px)
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, 0)
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes tout {
|
||||
to {
|
||||
opacity: 0
|
||||
}
|
||||
}
|
||||
432
www/css/detail.css
Normal file
@@ -0,0 +1,432 @@
|
||||
#ov {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 50;
|
||||
background: #000;
|
||||
overflow-y: auto;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transform: scale(1.04);
|
||||
transition: opacity .35s, transform .35s;
|
||||
scrollbar-width: none
|
||||
}
|
||||
|
||||
#ov.show {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
transform: none
|
||||
}
|
||||
|
||||
.dbl {
|
||||
position: fixed;
|
||||
inset: -60px;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
filter: blur(60px) brightness(.5) saturate(1)
|
||||
}
|
||||
|
||||
.dw {
|
||||
position: relative;
|
||||
min-height: 100vh
|
||||
}
|
||||
|
||||
.dimg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 82vh;
|
||||
background-size: cover;
|
||||
background-position: center top;
|
||||
-webkit-mask-image: linear-gradient(180deg, #000 45%, transparent 100%);
|
||||
mask-image: linear-gradient(180deg, #000 45%, transparent 100%)
|
||||
}
|
||||
|
||||
.dc {
|
||||
position: relative;
|
||||
padding: 40vh 0 8vh
|
||||
}
|
||||
|
||||
.dc>.dh {
|
||||
padding: 0 6vw
|
||||
}
|
||||
|
||||
.dh #dlogo {
|
||||
height: 20vh;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
margin-bottom: 2vh
|
||||
}
|
||||
|
||||
.dh #dlogo img {
|
||||
max-width: min(40vw, 540px);
|
||||
max-height: 20vh;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 4px 18px rgba(0, 0, 0, .6))
|
||||
}
|
||||
|
||||
.dh h1 {
|
||||
font-size: clamp(30px, 4.4vw, 76px);
|
||||
font-weight: 700;
|
||||
letter-spacing: -.02em;
|
||||
line-height: 1
|
||||
}
|
||||
|
||||
.dh .meta {
|
||||
margin-top: 1vh
|
||||
}
|
||||
|
||||
.dh .hb {
|
||||
margin: 2vh 0
|
||||
}
|
||||
|
||||
.circ {
|
||||
width: clamp(44px, 3.6vw, 64px);
|
||||
height: clamp(44px, 3.6vw, 64px);
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, .12);
|
||||
backdrop-filter: blur(12px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.circ.on {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.otx {
|
||||
max-width: min(70vw, 980px);
|
||||
font-size: clamp(13px, 1.1vw, 20px);
|
||||
line-height: 1.55;
|
||||
color: #d9d9e4;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden
|
||||
}
|
||||
|
||||
.otx.x {
|
||||
-webkit-line-clamp: unset;
|
||||
line-clamp: unset;
|
||||
display: block
|
||||
}
|
||||
|
||||
.more {
|
||||
display: inline-block;
|
||||
margin-top: 10px;
|
||||
background: rgba(255, 255, 255, .18);
|
||||
border-radius: 999px;
|
||||
padding: 5px 14px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.more.on {
|
||||
transform: scale(1.08);
|
||||
}
|
||||
|
||||
.ds {
|
||||
margin-top: 3vh
|
||||
}
|
||||
|
||||
.ds h3 {
|
||||
padding: 0 6vw;
|
||||
font-size: clamp(17px, 1.5vw, 26px);
|
||||
font-weight: 700
|
||||
}
|
||||
|
||||
.ds .row {
|
||||
padding-left: 6vw;
|
||||
padding-right: 6vw;
|
||||
min-height: 0
|
||||
}
|
||||
|
||||
.tr {
|
||||
flex: 0 0 clamp(250px, 21vw, 400px);
|
||||
border-radius: 20px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: #101016;
|
||||
padding: 0 0 18px;
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
.tr .tb {
|
||||
position: absolute;
|
||||
inset: -40px;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
filter: blur(34px) brightness(.62) saturate(1.35);
|
||||
transform: scale(1.2);
|
||||
z-index: -1
|
||||
}
|
||||
|
||||
.tr .th {
|
||||
aspect-ratio: 16/9;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
margin: 0 0 -26px;
|
||||
-webkit-mask-image: linear-gradient(180deg, #000 50%, rgba(0, 0, 0, .55) 74%, transparent 100%);
|
||||
mask-image: linear-gradient(180deg, #000 50%, rgba(0, 0, 0, .55) 74%, transparent 100%)
|
||||
}
|
||||
|
||||
.tr b,
|
||||
.tr small {
|
||||
display: block;
|
||||
padding: 0 18px;
|
||||
position: relative
|
||||
}
|
||||
|
||||
.tr b {
|
||||
font-size: clamp(14px, 1.1vw, 19px);
|
||||
font-weight: 600
|
||||
}
|
||||
|
||||
.tr small {
|
||||
color: #d0d0da;
|
||||
margin-top: 5px
|
||||
}
|
||||
|
||||
.tr.on {
|
||||
transform: scale(1.04);
|
||||
}
|
||||
|
||||
.cs {
|
||||
flex: 0 0 clamp(96px, 7.6vw, 150px);
|
||||
text-align: center;
|
||||
transition: transform .22s
|
||||
}
|
||||
|
||||
.cs .pf {
|
||||
width: clamp(90px, 7vw, 130px);
|
||||
height: clamp(90px, 7vw, 130px);
|
||||
border-radius: 50%;
|
||||
margin: 0 auto 10px;
|
||||
background: #2a2a3c center/cover;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.cs b {
|
||||
display: block;
|
||||
font-size: clamp(12px, .95vw, 16px)
|
||||
}
|
||||
|
||||
.cs small {
|
||||
display: block;
|
||||
color: #aaa;
|
||||
font-size: clamp(11px, .8vw, 14px);
|
||||
margin-top: 2px
|
||||
}
|
||||
|
||||
.cs.on {
|
||||
transform: scale(1.07)
|
||||
}
|
||||
|
||||
.circ {
|
||||
position: relative
|
||||
}
|
||||
|
||||
.circ svg {
|
||||
width: 46%;
|
||||
height: 46%
|
||||
}
|
||||
|
||||
.circ.sv {
|
||||
background: #fff;
|
||||
color: #000
|
||||
}
|
||||
|
||||
#dfv.sv svg {
|
||||
fill: currentColor
|
||||
}
|
||||
|
||||
.circ[data-l].on::after {
|
||||
content: attr(data-l);
|
||||
position: absolute;
|
||||
top: calc(100% + 12px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
white-space: nowrap;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
background: #fff;
|
||||
color: #000;
|
||||
padding: 5px 12px;
|
||||
border-radius: 999px;
|
||||
z-index: 5;
|
||||
pointer-events: none
|
||||
}
|
||||
|
||||
.mx {
|
||||
display: none;
|
||||
gap: 14px;
|
||||
align-items: center
|
||||
}
|
||||
|
||||
.mx.open {
|
||||
display: flex
|
||||
}
|
||||
|
||||
.mx.open .circ {
|
||||
animation: pop .32s cubic-bezier(.22, .9, .3, 1) backwards
|
||||
}
|
||||
|
||||
.mx.open .circ+.circ {
|
||||
animation-delay: .06s
|
||||
}
|
||||
|
||||
@keyframes pop {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(.4) translateX(-24px)
|
||||
}
|
||||
}
|
||||
|
||||
.eps {
|
||||
padding: 0 6vw
|
||||
}
|
||||
|
||||
.eh {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
margin-bottom: 14px
|
||||
}
|
||||
|
||||
.eps .eh h3 {
|
||||
padding: 0
|
||||
}
|
||||
|
||||
.dd {
|
||||
position: relative;
|
||||
z-index: 6
|
||||
}
|
||||
|
||||
.ddb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
height: 42px;
|
||||
padding: 0 18px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, .16);
|
||||
backdrop-filter: blur(12px);
|
||||
font-weight: 600;
|
||||
font-size: clamp(13px, 1vw, 17px);
|
||||
transition: background .2s, color .2s, transform .25s
|
||||
}
|
||||
|
||||
.ddb svg {
|
||||
width: 16px;
|
||||
height: 16px
|
||||
}
|
||||
|
||||
.ddb.on {
|
||||
background: #fff;
|
||||
color: #000;
|
||||
transform: scale(1.04)
|
||||
}
|
||||
|
||||
.ddl {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
left: 0;
|
||||
min-width: 240px;
|
||||
max-height: 46vh;
|
||||
overflow-y: auto;
|
||||
padding: 6px;
|
||||
border-radius: 16px;
|
||||
background: rgba(22, 22, 30, .97);
|
||||
scrollbar-width: none
|
||||
}
|
||||
|
||||
.ddl.open {
|
||||
display: block
|
||||
}
|
||||
|
||||
.ddi {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 10px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap
|
||||
}
|
||||
|
||||
.ddi small {
|
||||
color: rgba(255, 255, 255, .5)
|
||||
}
|
||||
|
||||
.ddi.on {
|
||||
background: #fff;
|
||||
color: #000
|
||||
}
|
||||
|
||||
.ddi.on small {
|
||||
color: rgba(0, 0, 0, .55)
|
||||
}
|
||||
|
||||
.el {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(clamp(210px, 17vw, 320px), 1fr));
|
||||
gap: 28px 20px;
|
||||
padding: 8px 6px 14px;
|
||||
margin: 0 -6px
|
||||
}
|
||||
|
||||
.el .none {
|
||||
grid-column: 1/-1
|
||||
}
|
||||
|
||||
.ep {
|
||||
min-width: 0
|
||||
}
|
||||
|
||||
.ep .et {
|
||||
position: relative;
|
||||
aspect-ratio: 16/9;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.ep .et img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block
|
||||
}
|
||||
|
||||
.ep b {
|
||||
display: block;
|
||||
margin-top: 12px;
|
||||
font-size: clamp(13px, 1vw, 16px);
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis
|
||||
}
|
||||
|
||||
.ep small {
|
||||
display: block;
|
||||
margin-top: 3px;
|
||||
font-size: clamp(12px, .9vw, 14px);
|
||||
color: rgba(255, 255, 255, .5);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis
|
||||
}
|
||||
|
||||
.ep.on .et {
|
||||
transform: scale(1.03)
|
||||
}
|
||||
65
www/css/library.css
Normal file
@@ -0,0 +1,65 @@
|
||||
#lib {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 25;
|
||||
background: linear-gradient(180deg, rgba(0, 0, 0, .25), rgba(0, 0, 0, .7));
|
||||
overflow-y: auto;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity .35s;
|
||||
padding: 9vh var(--pad) 10vh;
|
||||
scrollbar-width: none
|
||||
}
|
||||
|
||||
#lib.show {
|
||||
opacity: 1;
|
||||
pointer-events: auto
|
||||
}
|
||||
|
||||
.lib #main {
|
||||
visibility: hidden
|
||||
}
|
||||
|
||||
.lib #bg {
|
||||
filter: blur(70px) brightness(.5) saturate(1.5)
|
||||
}
|
||||
|
||||
.lib #dim {
|
||||
opacity: .35
|
||||
}
|
||||
|
||||
.lib #dim2 {
|
||||
opacity: 0 !important
|
||||
}
|
||||
|
||||
#lib h1 {
|
||||
font-size: clamp(28px, 3.4vw, 56px);
|
||||
font-weight: 700;
|
||||
letter-spacing: -.02em;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 18px
|
||||
}
|
||||
|
||||
#lib h1 small {
|
||||
font-size: clamp(13px, 1.1vw, 19px);
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, .6);
|
||||
letter-spacing: 0
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(var(--cw), 1fr));
|
||||
gap: 22px;
|
||||
padding: 30px 10px
|
||||
}
|
||||
|
||||
.grid .card {
|
||||
height: auto;
|
||||
aspect-ratio: 2/3
|
||||
}
|
||||
|
||||
.grid .none {
|
||||
grid-column: 1/-1
|
||||
}
|
||||
715
www/css/player.css
Normal file
@@ -0,0 +1,715 @@
|
||||
#pl {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 80;
|
||||
background: #000;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity .35s;
|
||||
}
|
||||
|
||||
#pl.show {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
#pl:not(.ui) {
|
||||
cursor: none;
|
||||
}
|
||||
|
||||
#pv {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
background: #000;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#pl.cover #pv {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
video::cue {
|
||||
font: 600 clamp(16px, 1.8vw, 30px) Inter, sans-serif;
|
||||
background: rgba(0, 0, 0, .55);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.sub-active::cue {
|
||||
color: var(--sub-color, #ffffff) !important;
|
||||
background-color: var(--sub-bg, rgba(0, 0, 0, 0.75)) !important;
|
||||
font-size: var(--sub-size, 22px) !important;
|
||||
text-shadow: var(--sub-shadow, 0 2px 4px rgba(0, 0, 0, 0.8)) !important;
|
||||
}
|
||||
|
||||
.sub-ctrl-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 14px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.sub-ctrl-row span {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.sub-btn-group {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.sub-btn-group button {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border: none;
|
||||
color: #fff;
|
||||
padding: 4px 10px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.sub-btn-group button.active {
|
||||
background: #fff;
|
||||
color: #000;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.src-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.src-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 44px;
|
||||
padding: 0 14px;
|
||||
border-radius: 12px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
|
||||
.src-header.on,
|
||||
.src-header:hover {
|
||||
background: #fff;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.src-expand-btn {
|
||||
font-size: 12px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
background: rgba(255, 255, 255, 0.16);
|
||||
color: inherit;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.src-sublist {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
padding-left: 14px;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.src-sublist.open {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.src-subitem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 38px;
|
||||
padding: 0 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.src-subitem.on,
|
||||
.src-subitem:hover {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#pl .top,
|
||||
#pl .bot,
|
||||
#pmid {
|
||||
opacity: 0;
|
||||
transition: opacity .3s, transform .25s, background .2s;
|
||||
}
|
||||
|
||||
#pl.ui .top,
|
||||
#pl.ui .bot,
|
||||
#pl.ui #pmid {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#pl .f {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
#pl:not(.ui) .f {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#pl .top,
|
||||
#pl .bot {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#pl .top {
|
||||
top: 0;
|
||||
padding: 2.6vh 2.4vw 14vh;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
background: linear-gradient(180deg, rgba(0, 0, 0, .65), transparent);
|
||||
}
|
||||
|
||||
#pl .bot {
|
||||
bottom: 0;
|
||||
padding: 10vh 2.4vw 2.6vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
background: linear-gradient(0deg, rgba(0, 0, 0, .7), transparent);
|
||||
}
|
||||
|
||||
.pic {
|
||||
width: clamp(44px, 3.3vw, 56px);
|
||||
height: clamp(44px, 3.3vw, 56px);
|
||||
flex: 0 0 auto;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
transition: background .2s, color .2s, transform .2s;
|
||||
}
|
||||
|
||||
.pic svg {
|
||||
width: 52%;
|
||||
height: 52%;
|
||||
}
|
||||
|
||||
.pic.on {
|
||||
background: #fff;
|
||||
color: #000;
|
||||
transform: scale(1.08);
|
||||
}
|
||||
|
||||
.av {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
margin: 3px 12px 0 6px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
background: #2a2a34;
|
||||
}
|
||||
|
||||
.pt {
|
||||
height: 46px;
|
||||
margin-top: 3px;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.pt b {
|
||||
font-size: clamp(15px, 1.2vw, 19px);
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 36vw;
|
||||
}
|
||||
|
||||
.pt small {
|
||||
font-size: clamp(12px, .9vw, 14px);
|
||||
color: rgba(255, 255, 255, .7);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.pr {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
#pmid {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 112px;
|
||||
height: 112px;
|
||||
margin: -56px 0 0 -56px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#pmid svg {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
filter: drop-shadow(0 2px 14px rgba(0, 0, 0, .45));
|
||||
}
|
||||
|
||||
#pmid.on {
|
||||
background: rgba(255, 255, 255, .18);
|
||||
transform: scale(1.08);
|
||||
}
|
||||
|
||||
.bt {
|
||||
font-size: 14px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
min-width: 44px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#pdur {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.sk {
|
||||
flex: 1;
|
||||
height: 40px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sk::before,
|
||||
.sk u,
|
||||
.sk i {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
height: 4px;
|
||||
margin-top: -2px;
|
||||
border-radius: 2px;
|
||||
transition: height .15s, margin .15s;
|
||||
}
|
||||
|
||||
.sk::before {
|
||||
right: 0;
|
||||
background: rgba(255, 255, 255, .2);
|
||||
}
|
||||
|
||||
.sk u {
|
||||
width: 0;
|
||||
background: rgba(255, 255, 255, .38);
|
||||
}
|
||||
|
||||
.sk i {
|
||||
width: 0;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.sk b {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: -8px 0 0 -8px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
transition: transform .15s;
|
||||
}
|
||||
|
||||
.sk.on::before,
|
||||
.sk.on u,
|
||||
.sk.on i {
|
||||
height: 6px;
|
||||
margin-top: -3px;
|
||||
}
|
||||
|
||||
.sk.on b {
|
||||
transform: scale(1.35);
|
||||
}
|
||||
|
||||
#pm {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: calc(2.6vh + 66px);
|
||||
right: 2.4vw;
|
||||
width: 320px;
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
border-radius: 18px;
|
||||
background: rgba(0, 0, 0, .94);
|
||||
backdrop-filter: blur(24px);
|
||||
z-index: 5;
|
||||
pointer-events: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
#pm.open {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#pm h4 {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: .08em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(255, 255, 255, .5);
|
||||
padding: 10px 12px 6px;
|
||||
}
|
||||
|
||||
#pm p {
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
color: rgba(255, 255, 255, .4);
|
||||
padding: 10px 12px 4px;
|
||||
}
|
||||
|
||||
.pi {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 48px;
|
||||
padding: 0 14px;
|
||||
border-radius: 12px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
transition: background .15s, color .15s;
|
||||
}
|
||||
|
||||
.pi svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.pi.on {
|
||||
background: #fff;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.vrow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 4px 6px 8px;
|
||||
}
|
||||
|
||||
.vrow span {
|
||||
width: 30px;
|
||||
text-align: right;
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, .7);
|
||||
}
|
||||
|
||||
.vs {
|
||||
flex: 1;
|
||||
height: 36px;
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.vs::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
height: 4px;
|
||||
margin-top: -2px;
|
||||
border-radius: 2px;
|
||||
background: rgba(255, 255, 255, .2);
|
||||
}
|
||||
|
||||
.vs i {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
height: 4px;
|
||||
margin-top: -2px;
|
||||
border-radius: 2px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.skip {
|
||||
display: none;
|
||||
height: 48px;
|
||||
padding: 0 26px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, .18);
|
||||
backdrop-filter: blur(16px);
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
transition: transform .25s, background .2s, color .2s;
|
||||
}
|
||||
|
||||
.skip.v {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.skip.on {
|
||||
background: #fff;
|
||||
color: #000;
|
||||
transform: scale(1.06);
|
||||
}
|
||||
|
||||
#pact {
|
||||
position: absolute;
|
||||
right: 2.4vw;
|
||||
bottom: 16vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.skip svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
#pnext {
|
||||
background: #fff;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
#pnext.on {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
#pl.eps .bot,
|
||||
#pl.eps #pmid,
|
||||
#pl.eps #pact {
|
||||
opacity: 0 !important;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#pep {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 9vh 0 3.4vh;
|
||||
background: linear-gradient(0deg, rgba(0, 0, 0, .97) 60%, rgba(0, 0, 0, .8) 82%, transparent);
|
||||
transform: translateY(40px);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: transform .45s cubic-bezier(.22, .9, .3, 1), opacity .3s, visibility 0s .45s;
|
||||
}
|
||||
|
||||
#pl.eps #pep {
|
||||
transform: none;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transition-delay: 0s;
|
||||
}
|
||||
|
||||
.peh {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 22px;
|
||||
padding: 0 2.4vw;
|
||||
}
|
||||
|
||||
.peh h3 {
|
||||
font-size: clamp(18px, 1.6vw, 26px);
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 14px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.peh h3 small {
|
||||
font-size: .62em;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, .55);
|
||||
max-width: 22vw;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
#pep #pseas {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 4px;
|
||||
margin: 0;
|
||||
min-height: 0;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
#pep .chip {
|
||||
height: 38px;
|
||||
padding: 0 18px;
|
||||
font-size: 14px;
|
||||
background: rgba(255, 255, 255, .12);
|
||||
}
|
||||
|
||||
#pep .chip.act {
|
||||
background: rgba(255, 255, 255, .3);
|
||||
}
|
||||
|
||||
#pep .chip.on {
|
||||
background: #fff;
|
||||
color: #000;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
#pep .pcar {
|
||||
padding: 18px 2.4vw 8px;
|
||||
min-height: 0;
|
||||
gap: 18px;
|
||||
-webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 5vw), transparent);
|
||||
mask-image: linear-gradient(90deg, #000 calc(100% - 5vw), transparent);
|
||||
}
|
||||
|
||||
.pe {
|
||||
flex: 0 0 clamp(230px, 19vw, 330px);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.pe .et {
|
||||
position: relative;
|
||||
aspect-ratio: 16/9;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.pe .et img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.pe .et>i {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 10px;
|
||||
font-style: normal;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
padding: 3px 9px;
|
||||
border-radius: 999px;
|
||||
background: rgba(0, 0, 0, .6);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.pe .pn {
|
||||
display: block;
|
||||
margin-top: 11px;
|
||||
font-size: clamp(13px, 1vw, 16px);
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.pe small {
|
||||
display: block;
|
||||
margin-top: 3px;
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, .5);
|
||||
}
|
||||
|
||||
.pe.on .et {
|
||||
transform: scale(1.04);
|
||||
}
|
||||
|
||||
.pe .pg {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 4px;
|
||||
background: rgba(255, 255, 255, .22);
|
||||
}
|
||||
|
||||
.pe .pg u,
|
||||
.eq u {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.pe .pg u {
|
||||
height: 100%;
|
||||
width: var(--p, 0%);
|
||||
}
|
||||
|
||||
.eq {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
bottom: 14px;
|
||||
display: flex;
|
||||
gap: 3px;
|
||||
align-items: flex-end;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.eq u {
|
||||
width: 3px;
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
animation: eq 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.eq u:nth-child(2) {
|
||||
animation-delay: .2s;
|
||||
}
|
||||
|
||||
.eq u:nth-child(3) {
|
||||
animation-delay: .4s;
|
||||
}
|
||||
|
||||
@keyframes eq {
|
||||
50% {
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
#pbg {
|
||||
position: absolute;
|
||||
inset: -60px;
|
||||
width: calc(100% + 120px);
|
||||
height: calc(100% + 120px);
|
||||
object-fit: cover;
|
||||
filter: blur(54px) brightness(0.42) saturate(1.45);
|
||||
transform: scale(1.08);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
opacity: 1;
|
||||
transition: opacity 0.4s ease;
|
||||
}
|
||||
|
||||
#pl.cover #pbg {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#pv {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
background: transparent;
|
||||
z-index: 1;
|
||||
}
|
||||
268
www/css/search.css
Normal file
@@ -0,0 +1,268 @@
|
||||
.srch #main {
|
||||
visibility: hidden
|
||||
}
|
||||
|
||||
.srch #bg {
|
||||
filter: blur(70px) brightness(.5) saturate(1.5)
|
||||
}
|
||||
|
||||
.srch #dim {
|
||||
opacity: .35
|
||||
}
|
||||
|
||||
.srch #dim2 {
|
||||
opacity: 0 !important
|
||||
}
|
||||
|
||||
#sr {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 25;
|
||||
background: linear-gradient(180deg, rgba(0, 0, 0, .2), rgba(0, 0, 0, .6));
|
||||
overflow-y: auto;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity .35s;
|
||||
padding: 6vh var(--pad) 8vh;
|
||||
scrollbar-width: none
|
||||
}
|
||||
|
||||
#sr.show {
|
||||
opacity: 1;
|
||||
pointer-events: auto
|
||||
}
|
||||
|
||||
.stop {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
height: clamp(46px, 4.6vw, 76px)
|
||||
}
|
||||
|
||||
.stop svg {
|
||||
width: clamp(30px, 2.8vw, 46px);
|
||||
height: clamp(30px, 2.8vw, 46px);
|
||||
color: rgba(255, 255, 255, .85);
|
||||
flex: 0 0 auto
|
||||
}
|
||||
|
||||
#stext {
|
||||
font-size: clamp(28px, 3vw, 52px);
|
||||
font-weight: 600;
|
||||
letter-spacing: -.02em;
|
||||
color: rgba(255, 255, 255, .5);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 70vw
|
||||
}
|
||||
|
||||
#stext.q {
|
||||
color: #fff
|
||||
}
|
||||
|
||||
#stext.q::after {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 3px;
|
||||
height: .8em;
|
||||
background: #fff;
|
||||
margin-left: 5px;
|
||||
vertical-align: -.08em;
|
||||
animation: bl 1s steps(1) infinite
|
||||
}
|
||||
|
||||
@keyframes bl {
|
||||
50% {
|
||||
opacity: 0
|
||||
}
|
||||
}
|
||||
|
||||
#kb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: clamp(2px, .35vw, 8px);
|
||||
margin: 1.4vh 0 0 4px;
|
||||
width: min(100%, 1180px)
|
||||
}
|
||||
|
||||
.k {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
max-width: 46px;
|
||||
height: clamp(34px, 3vw, 52px);
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: clamp(16px, 1.6vw, 26px);
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, .88);
|
||||
transition: background .2s, color .2s, transform .22s cubic-bezier(.22, .9, .3, 1)
|
||||
}
|
||||
|
||||
.k.w {
|
||||
flex: 0 0 auto;
|
||||
max-width: none;
|
||||
font-size: clamp(9px, .8vw, 13px);
|
||||
font-weight: 700;
|
||||
padding: 0 clamp(8px, .8vw, 14px);
|
||||
background: rgba(255, 255, 255, .22);
|
||||
letter-spacing: .04em;
|
||||
height: clamp(26px, 2.3vw, 40px)
|
||||
}
|
||||
|
||||
.k.d svg {
|
||||
width: 1.1em;
|
||||
height: 1.1em
|
||||
}
|
||||
|
||||
.k.on {
|
||||
background: #fff;
|
||||
color: #000;
|
||||
transform: scale(1.22);
|
||||
}
|
||||
|
||||
.row.ch {
|
||||
padding: 16px 6px 18px;
|
||||
min-height: 0;
|
||||
margin: 0 -6px;
|
||||
gap: 14px
|
||||
}
|
||||
|
||||
.chip {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 0 20px;
|
||||
height: clamp(36px, 3vw, 52px);
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, .24);
|
||||
color: #fff;
|
||||
font-size: clamp(14px, 1.3vw, 22px);
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
transition: background .2s, color .2s, transform .22s cubic-bezier(.22, .9, .3, 1)
|
||||
}
|
||||
|
||||
.chip svg {
|
||||
width: 1.1em;
|
||||
height: 1.1em
|
||||
}
|
||||
|
||||
.chip.on {
|
||||
background: #fff;
|
||||
color: #000;
|
||||
transform: scale(1.06)
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
border-top: 1px solid rgba(255, 255, 255, .22);
|
||||
margin-top: 1vh;
|
||||
padding-top: 1.6vh
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 8px 24px;
|
||||
border-radius: 999px;
|
||||
font-size: clamp(13px, 1.15vw, 19px);
|
||||
color: rgba(255, 255, 255, .65);
|
||||
position: relative;
|
||||
transition: background .2s, color .2s
|
||||
}
|
||||
|
||||
.tab.act {
|
||||
color: #fff;
|
||||
font-weight: 600
|
||||
}
|
||||
|
||||
.tab.act::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 24px;
|
||||
right: 24px;
|
||||
bottom: 0;
|
||||
height: 2px;
|
||||
background: #fff;
|
||||
border-radius: 2px
|
||||
}
|
||||
|
||||
.tab.on {
|
||||
background: rgba(255, 255, 255, .2);
|
||||
color: #fff
|
||||
}
|
||||
|
||||
.ss {
|
||||
margin-top: 2.6vh
|
||||
}
|
||||
|
||||
.ss h3 {
|
||||
font-size: clamp(14px, 1.2vw, 20px);
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, .75)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.sc {
|
||||
flex: 0 0 var(--cw);
|
||||
transition: transform .32s cubic-bezier(.22, .9, .3, 1)
|
||||
}
|
||||
|
||||
.sc .po {
|
||||
aspect-ratio: 2/3;
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
background: #1b1b24;
|
||||
}
|
||||
|
||||
.sc .po img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
filter: brightness(.86);
|
||||
transition: filter .3s
|
||||
}
|
||||
|
||||
.sc b {
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
font-size: clamp(13px, 1vw, 17px);
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis
|
||||
}
|
||||
|
||||
.sc small {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 3px;
|
||||
color: rgba(255, 255, 255, .62);
|
||||
font-size: clamp(11px, .85vw, 14px);
|
||||
white-space: nowrap
|
||||
}
|
||||
|
||||
.sc small span+span::before {
|
||||
content: "\2022";
|
||||
margin-right: 8px;
|
||||
opacity: .5
|
||||
}
|
||||
|
||||
.sc.on {
|
||||
transform: scale(1.05) translateY(-3px)
|
||||
}
|
||||
|
||||
.sc.on img {
|
||||
filter: none
|
||||
}
|
||||
114
www/css/sidebar.css
Normal file
@@ -0,0 +1,114 @@
|
||||
#side {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 340px;
|
||||
z-index: 30;
|
||||
pointer-events: none
|
||||
}
|
||||
|
||||
#side::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(90deg, rgba(0, 0, 0, .92) 0%, rgba(0, 0, 0, .7) 55%, transparent 100%);
|
||||
opacity: 0;
|
||||
transition: opacity .35s
|
||||
}
|
||||
|
||||
#side.open::before {
|
||||
opacity: 1
|
||||
}
|
||||
|
||||
#side nav {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
pointer-events: auto
|
||||
}
|
||||
|
||||
.ni {
|
||||
margin-left: 16px;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
padding: 0 16px;
|
||||
color: #fff;
|
||||
opacity: .45;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
transition: width .32s cubic-bezier(.2, .8, .2, 1), background .25s, color .25s, opacity .25s
|
||||
}
|
||||
|
||||
.ni svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
flex: 0 0 24px
|
||||
}
|
||||
|
||||
.ni span {
|
||||
font-size: 19px;
|
||||
font-weight: 600;
|
||||
opacity: 0;
|
||||
transition: opacity .25s
|
||||
}
|
||||
|
||||
.ni.act {
|
||||
opacity: 1
|
||||
}
|
||||
|
||||
#side.open .ni {
|
||||
width: 260px;
|
||||
opacity: .85
|
||||
}
|
||||
|
||||
#side.open .ni span {
|
||||
opacity: 1
|
||||
}
|
||||
|
||||
#side.open .ni.on {
|
||||
background: #fff;
|
||||
color: #000;
|
||||
opacity: 1
|
||||
}
|
||||
|
||||
.brand {
|
||||
position: absolute;
|
||||
left: 24px;
|
||||
top: 5vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
height: 44px;
|
||||
color: #fff
|
||||
}
|
||||
|
||||
.brand svg,
|
||||
.brand img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
flex: 0 0 40px;
|
||||
object-fit: contain
|
||||
}
|
||||
|
||||
.brand span {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -.03em;
|
||||
opacity: 0;
|
||||
transform: translateX(-6px);
|
||||
transition: opacity .3s, transform .3s
|
||||
}
|
||||
|
||||
#side.open .brand span {
|
||||
opacity: 1;
|
||||
transform: none
|
||||
}
|
||||
213
www/css/skeleton.css
vendored
Normal file
@@ -0,0 +1,213 @@
|
||||
.sb,
|
||||
.card:not(.lded),
|
||||
.sc .po:not(.lded),
|
||||
.ep .et:not(.lded),
|
||||
.cs.sl .pf,
|
||||
.trk,
|
||||
.chip.sl {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: #15151c
|
||||
}
|
||||
|
||||
.sb::before,
|
||||
.card:not(.lded)::before,
|
||||
.sc .po:not(.lded)::before,
|
||||
.ep .et:not(.lded)::before,
|
||||
.cs.sl .pf::before,
|
||||
.trk::before,
|
||||
.chip.sl::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(100deg, transparent 25%, rgba(255, 255, 255, .075) 50%, transparent 75%);
|
||||
transform: translateX(-100%);
|
||||
animation: shim 1.5s ease-in-out infinite
|
||||
}
|
||||
|
||||
@keyframes shim {
|
||||
to {
|
||||
transform: translateX(100%)
|
||||
}
|
||||
}
|
||||
|
||||
.card img,
|
||||
.sc .po img,
|
||||
.ep .et img {
|
||||
opacity: 0;
|
||||
transition: opacity .45s ease, filter .38s
|
||||
}
|
||||
|
||||
.card img.ld,
|
||||
.sc .po img.ld,
|
||||
.ep .et img.ld {
|
||||
opacity: 1
|
||||
}
|
||||
|
||||
.sb {
|
||||
display: block;
|
||||
border-radius: 8px;
|
||||
height: 14px
|
||||
}
|
||||
|
||||
.sec>.sb.h2 {
|
||||
width: 220px;
|
||||
height: 22px;
|
||||
margin: 0 var(--pad) 4px
|
||||
}
|
||||
|
||||
.ds>.sb.h3 {
|
||||
width: 180px;
|
||||
height: 22px;
|
||||
margin: 0 6vw 6px
|
||||
}
|
||||
|
||||
.ss>.sb.h3s {
|
||||
width: 160px;
|
||||
height: 18px
|
||||
}
|
||||
|
||||
.sc.sl .sb {
|
||||
width: 80%;
|
||||
height: 13px;
|
||||
margin-top: 10px
|
||||
}
|
||||
|
||||
.sc.sl .sb.s {
|
||||
width: 50%;
|
||||
height: 11px;
|
||||
margin-top: 6px
|
||||
}
|
||||
|
||||
.chip.sl {
|
||||
height: clamp(36px, 3vw, 52px)
|
||||
}
|
||||
|
||||
.trk {
|
||||
flex: 0 0 clamp(250px, 21vw, 400px);
|
||||
aspect-ratio: 16/10;
|
||||
border-radius: 20px
|
||||
}
|
||||
|
||||
.cs.sl {
|
||||
flex: 0 0 clamp(96px, 7.6vw, 150px)
|
||||
}
|
||||
|
||||
.hskel {
|
||||
display: none
|
||||
}
|
||||
|
||||
.hc.sl .hskel {
|
||||
display: block
|
||||
}
|
||||
|
||||
.hc.sl>:not(.hskel) {
|
||||
display: none
|
||||
}
|
||||
|
||||
.hskel .t,
|
||||
.dsk .t {
|
||||
width: min(32vw, 400px);
|
||||
height: 10vh;
|
||||
border-radius: 14px;
|
||||
margin-bottom: 3vh
|
||||
}
|
||||
|
||||
.hskel .m,
|
||||
.dsk .m {
|
||||
width: min(22vw, 300px);
|
||||
height: 18px;
|
||||
margin-bottom: 2vh
|
||||
}
|
||||
|
||||
.hskel .l,
|
||||
.dsk .l {
|
||||
width: min(44vw, 620px);
|
||||
height: 15px;
|
||||
margin-bottom: 10px
|
||||
}
|
||||
|
||||
.hskel .l.x,
|
||||
.dsk .l.x {
|
||||
width: min(26vw, 360px)
|
||||
}
|
||||
|
||||
.hskel .hb,
|
||||
.dsk .hb {
|
||||
margin: 3vh 0
|
||||
}
|
||||
|
||||
.hskel .b,
|
||||
.dsk .b {
|
||||
width: clamp(130px, 11vw, 190px);
|
||||
height: clamp(44px, 3.6vw, 64px);
|
||||
border-radius: 999px
|
||||
}
|
||||
|
||||
.hskel .b.c,
|
||||
.dsk .c {
|
||||
width: clamp(44px, 3.6vw, 64px);
|
||||
border-radius: 50%
|
||||
}
|
||||
|
||||
.dw {
|
||||
animation: fin .4s ease
|
||||
}
|
||||
|
||||
@keyframes fin {
|
||||
from {
|
||||
opacity: 0
|
||||
}
|
||||
}
|
||||
|
||||
#pspin {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin: -32px 0 0 -32px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid rgba(255, 255, 255, .2);
|
||||
border-top-color: #fff;
|
||||
animation: spin .8s linear infinite;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity .2s
|
||||
}
|
||||
|
||||
#pl.buf #pspin {
|
||||
opacity: 1
|
||||
}
|
||||
|
||||
#pl.buf #pmid {
|
||||
opacity: 0 !important
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg)
|
||||
}
|
||||
}
|
||||
|
||||
.pe .et:not(.lded)::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(100deg, transparent 25%, rgba(255, 255, 255, .075) 50%, transparent 75%);
|
||||
transform: translateX(-100%);
|
||||
animation: shim 1.5s ease-in-out infinite
|
||||
}
|
||||
|
||||
.pe .et:not(.lded) {
|
||||
background: #15151c
|
||||
}
|
||||
|
||||
.pe .et img {
|
||||
opacity: 0;
|
||||
transition: opacity .45s ease
|
||||
}
|
||||
|
||||
.pe .et img.ld {
|
||||
opacity: 1
|
||||
}
|
||||
90
www/index.html
Normal file
@@ -0,0 +1,90 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Vyla</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="css/base.css">
|
||||
<link rel="stylesheet" href="css/sidebar.css">
|
||||
<link rel="stylesheet" href="css/detail.css">
|
||||
<link rel="stylesheet" href="css/search.css">
|
||||
<link rel="stylesheet" href="css/library.css">
|
||||
<link rel="stylesheet" href="css/player.css">
|
||||
<link rel="stylesheet" href="css/skeleton.css">
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="bg"></div>
|
||||
<div id="dim"></div>
|
||||
<div id="dim2"></div>
|
||||
<div id="main">
|
||||
<div class="hero">
|
||||
<div id="hb"></div>
|
||||
<div class="hc sl">
|
||||
<div class="hskel"><i class="sb t"></i><i class="sb m"></i><i class="sb l"></i><i class="sb l"></i><i
|
||||
class="sb l x"></i>
|
||||
<div class="hb"><i class="sb b"></i><i class="sb b c"></i></div>
|
||||
</div>
|
||||
<div id="hlogo"></div>
|
||||
<div class="meta" id="hmeta"></div>
|
||||
<p class="ov1" id="hov"></p>
|
||||
<div class="hb"><button class="pb f" id="hplay">▶ Play</button><button class="ib f"
|
||||
id="hinfo">ⓘ More Info</button></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="rows"></div>
|
||||
<div id="sentinel"></div>
|
||||
</div>
|
||||
<div id="sr">
|
||||
<div class="stop"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="11" cy="11" r="7" />
|
||||
<path d="m20 20-3.5-3.5" />
|
||||
</svg><span id="stext">Search</span></div>
|
||||
<div id="kb"></div>
|
||||
<div class="row ch" id="chips"></div>
|
||||
<div class="tabs" id="tabs"></div>
|
||||
<div id="sres"></div>
|
||||
</div>
|
||||
<div id="lib">
|
||||
<h1 id="lt"></h1>
|
||||
<div class="grid" id="lgrid"></div>
|
||||
</div>
|
||||
<aside id="side">
|
||||
<div class="brand"><img id="mark" alt="Vyla"><span>Vyla</span></div>
|
||||
<nav id="nav"></nav>
|
||||
</aside>
|
||||
<div id="ov"></div>
|
||||
<div id="pl">
|
||||
<video id="pbg" playsinline preload="auto" muted></video>
|
||||
<video id="pv" playsinline preload="auto"></video>
|
||||
<div class="top" id="ptop"></div>
|
||||
<div class="f" id="pmid"></div>
|
||||
<div id="pact">
|
||||
<div class="skip f" id="pskip">Skip Intro</div>
|
||||
<div class="skip f" id="pnext"></div>
|
||||
</div>
|
||||
<div class="bot" id="pbot"></div>
|
||||
<div id="pep"></div>
|
||||
<div id="pspin"></div>
|
||||
<div id="pm"></div>
|
||||
</div>
|
||||
<script src="js/core.js"></script>
|
||||
<script src="js/skeleton.js"></script>
|
||||
<script src="js/focus.js"></script>
|
||||
<script src="js/library.js"></script>
|
||||
<script src="js/sidebar.js"></script>
|
||||
<script src="js/browse.js"></script>
|
||||
<script src="js/search.js"></script>
|
||||
<script src="js/detail.js"></script>
|
||||
<script src="js/player.js"></script>
|
||||
<script src="js/input.js"></script>
|
||||
<script src="js/main.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
57
www/js/browse.js
Normal file
@@ -0,0 +1,57 @@
|
||||
function hero(it) { clearTimeout(ht); ht = setTimeout(() => applyHero(it), 130) }
|
||||
async function applyHero(it) {
|
||||
if (heroIt === it) return; heroIt = it; $('.hc').classList.remove('sl');
|
||||
const bp = it.backdrop_path || it.poster_path;
|
||||
xf(bgb, I + 'w780' + bp); xf(hb, I + 'w1280' + bp);
|
||||
const t = it.title || it.name;
|
||||
$('#hlogo').innerHTML = `<h1>${t}</h1>`;
|
||||
$('#hmeta').innerHTML = `<span>★ ${(it.vote_average || 0).toFixed(1)}</span><span>${yr(it)}</span><span class="b">${it._t === 'movie' ? 'Movie' : 'Series'}</span><span>${gn(it)}</span>`;
|
||||
$('#hov').textContent = it.overview || '';
|
||||
const l = await logo(it);
|
||||
if (heroIt === it && l) $('#hlogo').innerHTML = `<img src="${l}" alt="${t.replace(/"/g, '')}">`
|
||||
}
|
||||
|
||||
function card(it, h = true) {
|
||||
const c = document.createElement('div'); c.className = 'card f'; c.tabIndex = -1;
|
||||
c.innerHTML = `<img loading="lazy" src="${I}w342${it.poster_path}" alt="">`;
|
||||
if (h) c._it = it; c._a = () => openDetail(it); return c
|
||||
}
|
||||
|
||||
function mkRow(i) {
|
||||
const mv = mode === "movie" || (mode === "discover" && i % 2 === 0), k = mode === "discover" ? i >> 1 : i, list = mv ? MG : TG, g = list[k % list.length], pg = 1 + Math.floor(k / list.length) % 20;
|
||||
const sec = document.createElement('section'); sec.className = 'sec';
|
||||
sec.innerHTML = `<h2>${g.name} ${mv ? 'Movies' : 'TV Shows'}</h2><div class="row">${SK.cards(14)}</div>`;
|
||||
rowsEl.appendChild(sec);
|
||||
return api(`/discover/${mv ? 'movie' : 'tv'}`, `&with_genres=${g.id}&sort_by=popularity.desc&vote_count.gte=100&include_adult=false&page=${pg}`).then(d => {
|
||||
const rs = (d.results || []).filter(x => x.poster_path);
|
||||
if (!rs.length) { sec.remove(); return }
|
||||
sec.lastChild.innerHTML = ''; rs.forEach(x => { x._t = mv ? 'movie' : 'tv'; sec.lastChild.appendChild(card(x)) })
|
||||
})
|
||||
}
|
||||
|
||||
async function more(n) {
|
||||
if (busy) return; busy = true; const g = gen;
|
||||
const ps = []; for (let j = 0; j < n; j++)ps.push(mkRow(idx++));
|
||||
await Promise.all(ps); if (g === gen) busy = false
|
||||
}
|
||||
|
||||
async function go(k) {
|
||||
if (k === 'search') { closeLib(true); openSearch(); return }
|
||||
closeSearch(true);
|
||||
if (k === 'wl' || k === 'fav') { openLib(k); return }
|
||||
closeLib(true);
|
||||
if (k === mode && rowsEl.children.length) { setAct(k); setFocus(document.querySelector('#rows .card')); return }
|
||||
mode = k; setAct(k); gen++; busy = false; idx = 0; heroIt = null; lastMain = null; rowsEl.innerHTML = ''; main.scrollTop = 0; $('.hc').classList.add('sl');
|
||||
await more(6);
|
||||
const first = document.querySelector('#rows .card');
|
||||
if (first) { applyHero(first._it); setFocus(first) }
|
||||
}
|
||||
|
||||
main.addEventListener('scroll', () => {
|
||||
const h = innerHeight;
|
||||
$('#dim2').style.opacity = Math.min(main.scrollTop / (h * .6), 1) * .55;
|
||||
if (main.scrollTop + h * 2.5 > main.scrollHeight) more(4)
|
||||
});
|
||||
|
||||
$('#hplay')._a = () => openPlayer(heroIt);
|
||||
$('#hinfo')._a = () => heroIt && openDetail(heroIt);
|
||||
37
www/js/core.js
Normal file
@@ -0,0 +1,37 @@
|
||||
const API_BASE = window.API_BASE || localStorage.getItem('vyla.api') || '';
|
||||
|
||||
const K = '3f6703f3e2a0b2b40fe4fbb3e1546244', A = 'https://api.themoviedb.org/3', I = 'https://image.tmdb.org/t/p/';
|
||||
const $ = s => document.querySelector(s);
|
||||
const main = $('#main'), rowsEl = $('#rows'), side = $('#side'), ov = $('#ov'), bgb = $('#bg'), hb = $('#hb'), sr = $('#sr'), sres = $('#sres'), chips = $('#chips'), tabs = $('#tabs'), kb = $('#kb'), pl = $('#pl'), vd = $('#pv');
|
||||
const api = async (p, q = '') => { try { const r = await fetch(`${A}${p}?api_key=${K}&language=en-US${q}`); return r.ok ? await r.json() : {} } catch (e) { return {} } };
|
||||
const ic = p => `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">${p}</svg>`;
|
||||
const P_WL = '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>', P_FV = '<path d="M20.8 5.6a5.5 5.5 0 0 0-7.8 0L12 6.7l-1-1.1a5.5 5.5 0 0 0-7.8 7.8l1 1.1L12 22l7.8-7.5 1-1.1a5.5 5.5 0 0 0 0-7.8z"/>';
|
||||
const LN = { wl: 'Watch Later', fav: 'Favorites' };
|
||||
const NAVEL = {}; let navAct = null, mode = '', gen = 0, sgen = 0, st;
|
||||
let fast = false, G = {}, MG = [], TG = [], cur = null, lastMain = null, heroIt = null, idx = 0, busy = false, ht;
|
||||
const LC = {};
|
||||
|
||||
function xf(box, url, cls = 'xf') {
|
||||
if (box._u === url) return; box._u = url;
|
||||
const im = new Image();
|
||||
im.onload = () => { if (box._u !== url) return; const d = document.createElement('div'); d.className = cls; d.style.backgroundImage = `url(${url})`; box.appendChild(d); requestAnimationFrame(() => requestAnimationFrame(() => d.classList.add('in'))); setTimeout(() => { while (box.children.length > 2) box.firstChild.remove() }, 800) };
|
||||
im.onerror = () => { if (box._u === url) box._u = null };
|
||||
im.src = url
|
||||
}
|
||||
|
||||
async function logo(it) {
|
||||
const k = it._t + it.id; if (k in LC) return LC[k];
|
||||
const d = await api(`/${it._t}/${it.id}/images`, '&include_image_language=en,null');
|
||||
const l = (d.logos || []).find(x => x.iso_639_1 === 'en') || (d.logos || [])[0];
|
||||
return LC[k] = l ? I + 'w500' + l.file_path : null
|
||||
}
|
||||
|
||||
const yr = it => ((it.release_date || it.first_air_date || '') + '').slice(0, 4);
|
||||
const gn = it => (it.genre_ids || []).map(i => G[i]).filter(Boolean).slice(0, 2).join(', ');
|
||||
|
||||
const esc = t => String(t).replace(/</g, '<');
|
||||
const mkf = (cls, html, fn) => { const e = document.createElement('div'); e.className = cls + ' f'; e.tabIndex = -1; e.innerHTML = html; e._a = fn; return e };
|
||||
const LOGO = 'assets/favicon.ico', PH = 'data:image/svg+xml,' + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><rect width="40" height="40" rx="11" fill="#2a2a34"/><circle cx="14" cy="14" r="3" fill="#5b5b6b"/><path d="M7 31l9-10 6 6 4-4 7 8z" fill="#5b5b6b"/></svg>');
|
||||
function toast(m) { const t = document.createElement('div'); t.className = 'toast'; t.textContent = m; (document.fullscreenElement || document.body).appendChild(t); setTimeout(() => t.remove(), 2300) }
|
||||
const imgDone = e => { const t = e.target; if (t.tagName === 'IMG') { t.classList.add('ld'); t.parentElement && t.parentElement.classList.add('lded') } };
|
||||
document.addEventListener('load', imgDone, true); document.addEventListener('error', imgDone, true);
|
||||
66
www/js/detail.js
Normal file
@@ -0,0 +1,66 @@
|
||||
let dgen = 0;
|
||||
async function openDetail(it) {
|
||||
const t = it._t;
|
||||
if (!ov.classList.contains('show')) ov._prev = cur;
|
||||
ov.classList.add('show'); side.classList.remove('open'); const my = ++dgen; ov.innerHTML = SK.detail(it); ov.scrollTop = 0;
|
||||
const d = await api(`/${t}/${it.id}`, `&append_to_response=videos,credits,recommendations,images,${t === 'movie' ? 'release_dates' : 'content_ratings'}&include_image_language=en,null`);
|
||||
if (my !== dgen || !ov.classList.contains('show')) return;
|
||||
if (!d.id) { closeDetail(); return }
|
||||
const title = d.title || d.name, year = ((d.release_date || d.first_air_date) || '').slice(0, 4);
|
||||
const cert = t === 'movie' ? (((d.release_dates || {}).results || []).find(r => r.iso_3166_1 === 'US') || { release_dates: [] }).release_dates.map(x => x.certification).find(Boolean) : (((d.content_ratings || {}).results || []).find(r => r.iso_3166_1 === 'US') || {}).rating;
|
||||
const rt = t === 'movie' ? (d.runtime ? `${Math.floor(d.runtime / 60)}h ${d.runtime % 60}m` : '') : (d.number_of_seasons ? `${d.number_of_seasons} Season${d.number_of_seasons > 1 ? 's' : ''}` : '');
|
||||
const lg = ((d.images || {}).logos || []).find(x => x.iso_639_1 === 'en') || ((d.images || {}).logos || [])[0];
|
||||
const bp = d.backdrop_path || d.poster_path;
|
||||
ov.innerHTML = `<div class="dbl" style="background-image:url(${I}w780${bp})"></div><div class="dw"><div class="dimg" style="background-image:url(${I}w1280${bp})"></div><div class="dc"><div class="dh"><div id="dlogo">${lg ? `<img src="${I}w500${lg.file_path}" alt="">` : `<h1>${title}</h1>`}</div><div class="meta"><span>${(d.vote_average || 0).toFixed(1)}</span><span>${year}</span>${cert ? `<span class="b">${cert}</span>` : ''}<span>${rt}</span><span>${(d.genres || []).slice(0, 3).map(g => g.name).join(', ')}</span></div><div class="hb"><button class="pb f" id="dplay">Play</button><div class="circ f" id="dmenu">⋮</div><div class="mx" id="dmx"><div class="circ f" id="dwl"></div><div class="circ f" id="dfv"></div></div></div><p class="otx" id="otx">${d.overview || ''}</p><span class="more f" id="dmore">MORE</span></div><div id="dsecs"></div></div></div>`;
|
||||
const secs = $('#dsecs');
|
||||
const rowOf = (name, kids) => { if (!kids.length) return; const s = document.createElement('section'); s.className = 'ds'; s.innerHTML = `<h3>${name}</h3><div class="row"></div>`; s.lastChild.append(...kids); secs.appendChild(s) };
|
||||
const el = (c, h) => { const e = document.createElement('div'); e.className = c + ' f'; e.tabIndex = -1; e.innerHTML = h; e._a = () => { }; return e };
|
||||
if (t === 'tv') {
|
||||
const ss = (d.seasons || []).filter(x => x.season_number > 0 && x.episode_count);
|
||||
if (ss.length) {
|
||||
const sec = document.createElement('section'); sec.className = 'ds eps';
|
||||
sec.innerHTML = '<div class="eh"><h3>Episodes</h3><div class="dd"><div class="ddb f" id="ddb"></div><div class="ddl" id="ddl"></div></div></div><div class="el" id="el"></div>';
|
||||
secs.appendChild(sec);
|
||||
const ddb = $('#ddb'), ddl = $('#ddl'), list = $('#el'); let cs = null;
|
||||
const nm = x => x.name || 'Season ' + x.season_number;
|
||||
const loadEps = async x => {
|
||||
cs = x; ddb.innerHTML = `<span>${esc(nm(x))}</span>` + ic('<path d="m6 9 6 6 6-6"/>');
|
||||
[...ddl.children].forEach((c, i) => c.classList.toggle('cur', ss[i] === x));
|
||||
list.innerHTML = SK.eps(10);
|
||||
const r = await api(`/tv/${it.id}/season/${x.season_number}`);
|
||||
if (cs !== x || !list.isConnected) return;
|
||||
list.innerHTML = '';
|
||||
(r.episodes || []).forEach((p, ix) => {
|
||||
const n = p.episode_number, img = p.still_path ? I + 'w300' + p.still_path : (it.backdrop_path ? I + 'w300' + it.backdrop_path : PH);
|
||||
const e = el('ep', `<div class="et"><img loading="lazy" src="${img}" alt=""></div><b>${esc(p.name || 'Episode ' + n)}</b><small>${['Episode ' + n, p.runtime ? p.runtime + 'm' : ''].filter(Boolean).join(' \u2022 ')}</small>`);
|
||||
e.title = p.overview || '';
|
||||
e._a = () => openPlayer(it, `S${x.season_number} E${n} \u2022 ${p.name || ''}`, { it, seasons: ss, s: x.season_number, eps: r.episodes, i: ix, cache: { [x.season_number]: r.episodes } }); list.appendChild(e)
|
||||
});
|
||||
const f0 = list.firstChild; if (f0 && f0._a && $('#dplay')) $('#dplay')._a = () => f0._a();
|
||||
if (!list.children.length) list.innerHTML = '<p class="none">No episodes listed.</p>'
|
||||
};
|
||||
ss.forEach(x => ddl.appendChild(mkf('ddi', `<span>${esc(nm(x))}</span><small>${x.episode_count} eps</small>`, () => { closeDD(); loadEps(x) })));
|
||||
ddb._a = () => { if (!closeDD()) { ddl.classList.add('open'); setFocus(ddl.children[Math.max(0, ss.indexOf(cs))]) } };
|
||||
loadEps(ss[0])
|
||||
}
|
||||
}
|
||||
rowOf('Trailers', ((d.videos || {}).results || []).filter(v => v.site === 'YouTube' && /Trailer|Teaser|Clip/.test(v.type)).sort((a, b) => (b.type === 'Trailer') - (a.type === 'Trailer')).slice(0, 8).map(v => el('tr', `<div class="tb" style="background-image:url(https://img.youtube.com/vi/${v.key}/mqdefault.jpg)"></div><div class="th" style="background-image:url(https://img.youtube.com/vi/${v.key}/mqdefault.jpg)"></div><b>${v.name}</b><small>▶ ${v.type}</small>`)));
|
||||
rowOf('Related', ((d.recommendations || {}).results || []).filter(x => x.poster_path).map(x => { x._t = t; return card(x, false) }));
|
||||
rowOf('Cast', ((d.credits || {}).cast || []).slice(0, 20).map(p => el('cs', `<div class="pf" style="${p.profile_path ? `background-image:url(${I}w185${p.profile_path})` : ''}">${p.profile_path ? '' : p.name[0]}</div><b>${p.name}</b><small>${p.character || ''}</small>`)));
|
||||
$('#dplay')._a = () => openPlayer(it); const sync = () => [['dwl', 'wl', P_WL], ['dfv', 'fav', P_FV]].forEach(([id, k, p]) => { const e = $('#' + id), v = has(k, it); e.classList.toggle('sv', v); e.dataset.l = (v ? 'Remove from ' : 'Add to ') + LN[k]; e.innerHTML = ic(p) });
|
||||
sync();
|
||||
$('#dmenu')._a = () => { const o = $('#dmx').classList.toggle('open'); $('#dmenu').classList.toggle('sv', o) };
|
||||
$('#dwl')._a = () => { toggleLib('wl', it); sync() }; $('#dfv')._a = () => { toggleLib('fav', it); sync() };
|
||||
$('#dmore')._a = () => { const o = $('#otx'), m = $('#dmore'); o.classList.toggle('x'); m.textContent = o.classList.contains('x') ? 'LESS' : 'MORE' };
|
||||
ov.scrollTop = 0;
|
||||
setFocus($('#dplay'))
|
||||
}
|
||||
|
||||
function closeDetail() {
|
||||
ov.classList.remove('show');
|
||||
const p = ov._prev;
|
||||
setTimeout(() => { if (!ov.classList.contains('show')) ov.innerHTML = '' }, 400);
|
||||
if (p && document.body.contains(p)) setFocus(p); else if (lib.classList.contains('show')) setFocus($('#lgrid .card') || navAct)
|
||||
}
|
||||
|
||||
function closeDD() { const l = $('#ddl'); if (l && l.classList.contains('open')) { l.classList.remove('open'); setFocus($('#ddb')); return true } return false }
|
||||
60
www/js/focus.js
Normal file
@@ -0,0 +1,60 @@
|
||||
function setFocus(el) {
|
||||
if (!el) return;
|
||||
const od = $('#ddl.open'); if (od && !el.closest('.dd')) od.classList.remove('open');
|
||||
cur && cur.classList.remove('on'); cur = el; el.classList.add('on');
|
||||
const inSide = !!el.closest('#side'), inOv = !!el.closest('#ov');
|
||||
side.classList.toggle('open', inSide && !ov.classList.contains('show'));
|
||||
if (!inSide && !inOv && !el.closest('#pl')) lastMain = el;
|
||||
const row = el.closest('.row');
|
||||
if (row) row.scrollTo({ left: Math.max(0, el.offsetLeft - parseInt(getComputedStyle(row).paddingLeft)), behavior: fast ? 'auto' : 'smooth' });
|
||||
|
||||
const menu = el.closest('#pm');
|
||||
if (menu) {
|
||||
el.scrollIntoView({ block: 'nearest', behavior: fast ? 'auto' : 'smooth' });
|
||||
return;
|
||||
}
|
||||
|
||||
const scr = el.closest('#ov,#sr,#lib');
|
||||
if (scr && el.closest('#kb,#chips,#tabs')) scr.scrollTo({ top: 0, behavior: fast ? 'auto' : 'smooth' });
|
||||
else if (scr) {
|
||||
const r = el.getBoundingClientRect();
|
||||
if (r.bottom > innerHeight - 50) scr.scrollBy({ top: r.bottom - innerHeight + 110, behavior: fast ? 'auto' : 'smooth' });
|
||||
else if (r.top < 50) scr.scrollBy({ top: r.top - 110, behavior: fast ? 'auto' : 'smooth' });
|
||||
}
|
||||
else if (!inSide && !el.closest('#pl')) {
|
||||
const sec = el.closest('.sec');
|
||||
main.scrollTo({ top: sec ? Math.max(0, sec.offsetTop - innerHeight * .52) : 0, behavior: fast ? 'auto' : 'smooth' });
|
||||
}
|
||||
if (el._it) hero(el._it); if (el._bg) sbgf(el._bg);
|
||||
}
|
||||
|
||||
function nav(d) {
|
||||
const sc = pl.classList.contains('show') ? (pl.classList.contains('eps') ? '#pep .f' : '#pl .f') : ov.classList.contains('show') ? '#ov .f' : sr.classList.contains('show') ? '#sr .f,#side .f' : lib.classList.contains('show') ? '#lib .f,#side .f' : '#side .f,#main .f';
|
||||
const c = cur.getBoundingClientRect(), cx = c.left + c.width / 2, cy = c.top + c.height / 2;
|
||||
const inSide = !!cur.closest('#side');
|
||||
if (inSide && d === 'r' && lastMain && getComputedStyle(lastMain).visibility !== 'hidden') { setFocus(lastMain); return }
|
||||
if ((d === 'l' || d === 'r') && !inSide) {
|
||||
const g = cur.closest('.row,#kb,#tabs');
|
||||
if (g) {
|
||||
const L = [...g.children].filter(e => e.classList.contains('f') && e.getBoundingClientRect().width), i = L.indexOf(cur), n = L[i + (d === 'r' ? 1 : -1)];
|
||||
if (i >= 0) { if (n) setFocus(n); else if (d === 'l' && !cur.closest('#ov,#pl')) setFocus(navAct); return }
|
||||
}
|
||||
}
|
||||
let best = null, bs = 1e9;
|
||||
document.querySelectorAll(sc).forEach(e => {
|
||||
if (e === cur) return;
|
||||
const r = e.getBoundingClientRect(); if (!r.width) return;
|
||||
const x = r.left + r.width / 2, y = r.top + r.height / 2, dx = x - cx, dy = y - cy;
|
||||
let p, s;
|
||||
if (d === 'r') { if (dx < 8) return; p = dx; s = Math.abs(dy) * 2.5 }
|
||||
else if (d === 'l') { if (dx > -8) return; p = -dx; s = Math.abs(dy) * 2.5 }
|
||||
else if (d === 'd') { if (dy < 8) return; p = dy; s = Math.abs(dx) * 2 }
|
||||
else { if (dy > -8) return; p = -dy; s = Math.abs(dx) * 2 }
|
||||
if (p + s < bs) { bs = p + s; best = e }
|
||||
});
|
||||
if (!best) return;
|
||||
if (best.closest('#side') && !inSide) best = navAct;
|
||||
setFocus(best)
|
||||
}
|
||||
|
||||
function act(el) { el && el._a && el._a() }
|
||||
99
www/js/input.js
Normal file
@@ -0,0 +1,99 @@
|
||||
const KM = {
|
||||
37: 'l', 21: 'l',
|
||||
38: 'u', 19: 'u',
|
||||
39: 'r', 22: 'r',
|
||||
40: 'd', 20: 'd',
|
||||
13: 'e', 23: 'e', 66: 'e',
|
||||
27: 'b', 8: 'b', 4: 'b', 10009: 'b', 461: 'b'
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', e => {
|
||||
fast = e.repeat;
|
||||
document.body.classList.add('kbm');
|
||||
|
||||
if (e.keyCode === 85 || e.keyCode === 179) {
|
||||
e.preventDefault();
|
||||
tgl();
|
||||
return;
|
||||
}
|
||||
if (e.keyCode === 126 || e.keyCode === 250) {
|
||||
e.preventDefault();
|
||||
vd.play();
|
||||
return;
|
||||
}
|
||||
if (e.keyCode === 127) {
|
||||
e.preventDefault();
|
||||
vd.pause();
|
||||
return;
|
||||
}
|
||||
if (e.keyCode === 89 || e.keyCode === 227) {
|
||||
e.preventDefault();
|
||||
vd.currentTime = Math.max(0, vd.currentTime - 10);
|
||||
return;
|
||||
}
|
||||
if (e.keyCode === 90 || e.keyCode === 228) {
|
||||
e.preventDefault();
|
||||
vd.currentTime = Math.min(vd.duration || Infinity, vd.currentTime + 10);
|
||||
return;
|
||||
}
|
||||
|
||||
if (pl.classList.contains('show')) {
|
||||
showCtl();
|
||||
if (e.key === ' ' || e.keyCode === 32) { e.preventDefault(); tgl(); return }
|
||||
if (cur === $('#pseek') && (e.keyCode === 37 || e.keyCode === 39 || e.keyCode === 21 || e.keyCode === 22)) {
|
||||
e.preventDefault();
|
||||
vd.currentTime += (e.keyCode === 39 || e.keyCode === 22) ? 10 : -10;
|
||||
return;
|
||||
}
|
||||
if (cur && cur.id === 'vsl' && (e.keyCode === 37 || e.keyCode === 39 || e.keyCode === 21 || e.keyCode === 22)) {
|
||||
e.preventDefault();
|
||||
vd.muted = false;
|
||||
vd.volume = Math.min(1, Math.max(0, vd.volume + ((e.keyCode === 39 || e.keyCode === 22) ? .1 : -.1)));
|
||||
cur._v();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (sr.classList.contains('show') && !ov.classList.contains('show') && !pl.classList.contains('show')) {
|
||||
if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) { e.preventDefault(); setQ(sqv + e.key); return }
|
||||
if (e.keyCode === 8) { e.preventDefault(); setQ(sqv.slice(0, -1)); return }
|
||||
}
|
||||
|
||||
const k = KM[e.keyCode];
|
||||
if (!k) return;
|
||||
e.preventDefault();
|
||||
|
||||
if (!cur) {
|
||||
const fallback = document.querySelector('.f');
|
||||
if (fallback) setFocus(fallback);
|
||||
return;
|
||||
}
|
||||
|
||||
if (k === 'e') act(cur);
|
||||
else if (k === 'b') {
|
||||
if (pl.classList.contains('show')) {
|
||||
pmK ? closeMenu(1) : pl.classList.contains('eps') ? closeEps(1) : closePlayer();
|
||||
} else if (ov.classList.contains('show')) {
|
||||
closeDD() || closeDetail();
|
||||
} else if (lib.classList.contains('show')) {
|
||||
closeLib();
|
||||
} else if (sr.classList.contains('show')) {
|
||||
closeSearch();
|
||||
}
|
||||
} else {
|
||||
nav(k);
|
||||
}
|
||||
});
|
||||
|
||||
let mx = -1, my = -1;
|
||||
document.addEventListener('mousemove', e => {
|
||||
if (Math.abs(e.clientX - mx) + Math.abs(e.clientY - my) < 4) return;
|
||||
mx = e.clientX; my = e.clientY; document.body.classList.remove('kbm');
|
||||
if (pl.classList.contains('show')) { showCtl(); const g = e.target.closest('.f'); if (g && g !== cur && pl.contains(g)) setFocus(g); return }
|
||||
const f = e.target.closest('.f');
|
||||
if (f && f !== cur && (!ov.classList.contains('show') || ov.contains(f))) setFocus(f)
|
||||
});
|
||||
document.addEventListener('click', e => {
|
||||
const f = e.target.closest('.f'); if (f) act(f)
|
||||
});
|
||||
ov.addEventListener('wheel', () => { }, { passive: true });
|
||||
27
www/js/library.js
Normal file
@@ -0,0 +1,27 @@
|
||||
let LIB = { wl: [], fav: [] }, libK = null; const lib = $('#lib');
|
||||
try { Object.assign(LIB, JSON.parse(localStorage.getItem('vyla.lib') || '{}')) } catch (e) { }
|
||||
const saveLib = () => { try { localStorage.setItem('vyla.lib', JSON.stringify(LIB)) } catch (e) { } };
|
||||
const slim = it => ({ id: it.id, _t: it._t || it.media_type, title: it.title, name: it.name, poster_path: it.poster_path, backdrop_path: it.backdrop_path, release_date: it.release_date, first_air_date: it.first_air_date, vote_average: it.vote_average, overview: it.overview });
|
||||
const has = (k, it) => LIB[k].some(x => x.id === it.id && x._t === it._t);
|
||||
function toggleLib(k, it) {
|
||||
const i = LIB[k].findIndex(x => x.id === it.id && x._t === it._t), had = i >= 0;
|
||||
if (had) LIB[k].splice(i, 1); else LIB[k].unshift(slim(it));
|
||||
saveLib(); syncNav(); toast((had ? 'Removed from ' : 'Added to ') + LN[k]);
|
||||
if (lib.classList.contains('show')) drawLib()
|
||||
}
|
||||
function drawLib() {
|
||||
const k = libK, a = LIB[k], g = $('#lgrid');
|
||||
$('#lt').innerHTML = `${LN[k]}<small>${a.length} title${a.length === 1 ? '' : 's'}</small>`;
|
||||
g.innerHTML = '';
|
||||
if (!a.length) { g.innerHTML = '<p class="none">Nothing here yet. Open a title, press the \u22EE button and add it.</p>'; return }
|
||||
a.forEach(x => { const c = card(x, false); c._bg = x; g.appendChild(c) })
|
||||
}
|
||||
function openLib(k) {
|
||||
libK = k; setAct(k); document.body.classList.add('lib'); lib.classList.add('show'); side.classList.remove('open'); lib.scrollTop = 0;
|
||||
drawLib(); syncNav(); setFocus($('#lgrid .card') || NAVEL[k])
|
||||
}
|
||||
function closeLib(silent) {
|
||||
if (!lib.classList.contains('show')) return;
|
||||
lib.classList.remove('show'); document.body.classList.remove('lib'); libK = null; heroIt = null; bgb._u = null; syncNav();
|
||||
if (!silent) { setAct(mode); setFocus($('#rows .card')) }
|
||||
}
|
||||
8
www/js/main.js
Normal file
@@ -0,0 +1,8 @@
|
||||
(async () => {
|
||||
rowsEl.innerHTML = SK.rows(4);
|
||||
const [m, t] = await Promise.all([api('/genre/movie/list'), api('/genre/tv/list')]);
|
||||
MG = m.genres || []; TG = t.genres || [];
|
||||
[...MG, ...TG].forEach(g => G[g.id] = g.name);
|
||||
if (!MG.length || !TG.length) { rowsEl.innerHTML = '<p style="padding:40vh var(--pad)">Could not reach TMDB.</p>'; return }
|
||||
await go('discover');
|
||||
})();
|
||||
805
www/js/player.js
Normal file
@@ -0,0 +1,805 @@
|
||||
let currentHls = null;
|
||||
let currentBgHls = null;
|
||||
let currentSources = [];
|
||||
let resolvedStreams = [];
|
||||
let selectedSourceKey = null;
|
||||
let selectedStreamUrl = null;
|
||||
let expandedSources = new Set();
|
||||
let currentQualityLevels = [];
|
||||
let selectedQualityIndex = -1;
|
||||
let activeSubtitles = [];
|
||||
let mediaSegments = [];
|
||||
let activeSegment = null;
|
||||
let plPrev = null;
|
||||
let plT = null;
|
||||
let plItem = null;
|
||||
let pmK = null;
|
||||
let plCtx = null;
|
||||
let viewS = null;
|
||||
let drag = 0;
|
||||
let discoverySession = 0;
|
||||
let userHasManuallySelectedSource = false;
|
||||
|
||||
let subSettings = {
|
||||
size: 24,
|
||||
color: '#ffffff',
|
||||
bg: 'rgba(0,0,0,0.75)',
|
||||
offset: 0
|
||||
};
|
||||
|
||||
const pi = (p, f) => `<svg viewBox="0 0 24 24" fill="${f ? 'currentColor' : 'none'}" stroke="${f ? 'none' : 'currentColor'}" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">${p}</svg>`;
|
||||
const SPK = '<path d="M4 9v6h4l5 4V5L8 9z"/>';
|
||||
const PI = {
|
||||
x: pi('<path d="M5 5l14 14M19 5 5 19"/>'),
|
||||
play: pi('<path d="M7 4.5v15L19.5 12z" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/>', 1),
|
||||
pause: pi('<rect x="6" y="4.5" width="4" height="15" rx="1.2"/><rect x="14" y="4.5" width="4" height="15" rx="1.2"/>', 1),
|
||||
cc: pi('<rect x="3" y="5" width="18" height="14" rx="3"/><path d="M10.5 10a2 2 0 1 0 0 4M17 10a2 2 0 1 0 0 4"/>'),
|
||||
vol: pi(SPK + '<path d="M16.5 8.5a5 5 0 0 1 0 7M19 6a9 9 0 0 1 0 12"/>'),
|
||||
mute: pi(SPK + '<path d="m17 9.5 4 5m0-5-4 5"/>'),
|
||||
qlt: pi('<path d="M4 6h16M4 12h16M4 18h7"/>'),
|
||||
set: pi('<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"/>'),
|
||||
more: pi('<path d="M5 12h.01M12 12h.01M19 12h.01" stroke-width="3.4"/>'),
|
||||
fs: pi('<path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5"/>'),
|
||||
fx: pi('<path d="M9 4v5H4M15 4v5h5M9 20v-5H4M15 20v-5h5"/>'),
|
||||
eps: pi('<rect x="3" y="4" width="7" height="6" rx="1.5"/><rect x="14" y="4" width="7" height="6" rx="1.5"/><rect x="3" y="14" width="7" height="6" rx="1.5"/><rect x="14" y="14" width="7" height="6" rx="1.5"/>'),
|
||||
chk: pi('<path d="m5 12.5 4.5 4.5L19 7"/>')
|
||||
};
|
||||
|
||||
$('#ptop').innerHTML = `<div class="pic f" id="pback">${PI.x}</div><img class="av" id="pav" alt=""><div class="pt"><b id="pt"></b><small id="ps"></small></div><div class="pr"><div class="pic f" id="pqlt" title="Quality">${PI.qlt}</div><div class="pic f" id="pcc">${PI.cc}</div><div class="pic f" id="pvol">${PI.vol}</div><div class="pic f" id="pset">${PI.set}</div><div class="pic f" id="pmore">${PI.more}</div></div>`;
|
||||
$('#pbot').innerHTML = `<span class="bt" id="pcur">0:00</span><div class="sk f" id="pseek"><u id="pbuf"></u><i id="pfill"></i><b id="phd"></b></div><span class="bt" id="pdur">0:00</span><div class="pic f" id="peps">${PI.eps}</div><div class="pic f" id="pfs">${PI.fs}</div>`;
|
||||
$('#pmid').innerHTML = PI.play;
|
||||
|
||||
const pm = $('#pm');
|
||||
const sk = $('#pseek');
|
||||
const pbg = $('#pbg');
|
||||
|
||||
const fmt = t => {
|
||||
t = Math.max(0, Math.floor(t || 0));
|
||||
const h = Math.floor(t / 3600);
|
||||
const m = Math.floor((t % 3600) / 60);
|
||||
const c = t % 60;
|
||||
return (h ? h + ':' + String(m).padStart(2, '0') : m) + ':' + String(c).padStart(2, '0');
|
||||
};
|
||||
|
||||
const tgl = () => vd.paused ? vd.play() : vd.pause();
|
||||
const pico = () => {
|
||||
$('#pmid').innerHTML = vd.paused ? PI.play : PI.pause;
|
||||
};
|
||||
|
||||
function showCtl() {
|
||||
pl.classList.add('ui');
|
||||
clearTimeout(plT);
|
||||
plT = setTimeout(() => {
|
||||
if (!vd.paused && !pmK && !pl.classList.contains('eps')) {
|
||||
pl.classList.remove('ui');
|
||||
}
|
||||
}, 3800);
|
||||
}
|
||||
|
||||
function closeMenu(rf) {
|
||||
if (!pmK) return;
|
||||
const t = {
|
||||
qlt: '#pqlt',
|
||||
cc: '#pcc',
|
||||
vol: '#pvol',
|
||||
set: '#pset',
|
||||
more: '#pmore'
|
||||
}[pmK];
|
||||
pmK = null;
|
||||
pm.classList.remove('open');
|
||||
pm.innerHTML = '';
|
||||
if (rf) setFocus($(t));
|
||||
}
|
||||
|
||||
function updateSubStyles() {
|
||||
vd.classList.add('sub-active');
|
||||
vd.style.setProperty('--sub-size', `${subSettings.size}px`);
|
||||
vd.style.setProperty('--sub-color', subSettings.color);
|
||||
vd.style.setProperty('--sub-bg', subSettings.bg);
|
||||
Array.from(vd.textTracks).forEach(track => {
|
||||
if (track.cues) {
|
||||
for (let i = 0; i < track.cues.length; i++) {
|
||||
const cue = track.cues[i];
|
||||
if (cue && !cue._initialOffsetApplied) {
|
||||
cue.startTime += subSettings.offset;
|
||||
cue.endTime += subSettings.offset;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function applySubOffset(delta) {
|
||||
subSettings.offset += delta;
|
||||
Array.from(vd.textTracks).forEach(track => {
|
||||
if (track.cues) {
|
||||
for (let i = 0; i < track.cues.length; i++) {
|
||||
const cue = track.cues[i];
|
||||
if (cue) {
|
||||
cue.startTime += delta;
|
||||
cue.endTime += delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function buildProxyUrl(rawUrl, headersObj = {}) {
|
||||
return `${API_BASE}/api/proxy?url=${encodeURIComponent(rawUrl)}&headers=${encodeURIComponent(JSON.stringify(headersObj))}`;
|
||||
}
|
||||
|
||||
async function fetchSources() {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/sources`);
|
||||
const data = await res.json();
|
||||
currentSources = data.sources || [];
|
||||
} catch (_) {
|
||||
currentSources = [];
|
||||
}
|
||||
}
|
||||
|
||||
async function startConcurrentDiscovery(it, s = null, e = null) {
|
||||
const session = ++discoverySession;
|
||||
pl.classList.add('buf');
|
||||
|
||||
resolvedStreams = [];
|
||||
selectedStreamUrl = null;
|
||||
let foundFirstPlayable = false;
|
||||
|
||||
if (!currentSources.length) {
|
||||
await fetchSources();
|
||||
}
|
||||
|
||||
if (session !== discoverySession) return;
|
||||
|
||||
const sourcePromises = currentSources.map(async (src) => {
|
||||
let epQuery = s && e ? `&season=${s}&episode=${e}` : '';
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/stream?provider=${src.key}&tmdbId=${it.id}${epQuery}`);
|
||||
const data = await res.json();
|
||||
if (session !== discoverySession) return;
|
||||
|
||||
if (data.success && data.stream) {
|
||||
const streamData = data.stream;
|
||||
const headers = streamData.headers || {};
|
||||
let playable = [];
|
||||
|
||||
if (Array.isArray(streamData.allUrls) && streamData.allUrls.length > 0) {
|
||||
playable = streamData.allUrls;
|
||||
} else if (streamData.url) {
|
||||
playable = [{ url: streamData.url, quality: streamData.quality || 'Auto', isM3U8: streamData.isM3U8 }];
|
||||
}
|
||||
|
||||
if (playable.length > 0) {
|
||||
const resolvedEntry = {
|
||||
provider: src.key,
|
||||
label: src.label || src.key,
|
||||
headers,
|
||||
primaryUrl: playable[0].url,
|
||||
allUrls: playable
|
||||
};
|
||||
|
||||
resolvedStreams.push(resolvedEntry);
|
||||
|
||||
if (pmK === 'set') {
|
||||
renderSourcesMenu();
|
||||
}
|
||||
|
||||
if (!foundFirstPlayable && !userHasManuallySelectedSource) {
|
||||
foundFirstPlayable = true;
|
||||
selectedSourceKey = src.key;
|
||||
playSpecificStream(resolvedEntry.primaryUrl, resolvedEntry.headers);
|
||||
} else if (userHasManuallySelectedSource && selectedSourceKey === src.key && !selectedStreamUrl) {
|
||||
playSpecificStream(resolvedEntry.primaryUrl, resolvedEntry.headers);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_) { }
|
||||
});
|
||||
|
||||
Promise.allSettled(sourcePromises).then(() => {
|
||||
if (session !== discoverySession) return;
|
||||
if (!resolvedStreams.length) {
|
||||
pl.classList.remove('buf');
|
||||
toast('No playable sources available.');
|
||||
}
|
||||
});
|
||||
|
||||
loadSubtitles(it.id, s, e);
|
||||
loadTimestamps(it.id, s, e);
|
||||
}
|
||||
|
||||
function playSpecificStream(streamUrl, headers = {}) {
|
||||
pl.classList.add('buf');
|
||||
selectedStreamUrl = streamUrl;
|
||||
|
||||
if (currentHls) {
|
||||
currentHls.destroy();
|
||||
currentHls = null;
|
||||
}
|
||||
if (currentBgHls) {
|
||||
currentBgHls.destroy();
|
||||
currentBgHls = null;
|
||||
}
|
||||
|
||||
const finalUrl = buildProxyUrl(streamUrl, headers);
|
||||
|
||||
if (streamUrl.includes('.m3u8')) {
|
||||
if (Hls.isSupported()) {
|
||||
currentHls = new Hls({
|
||||
xhrSetup: function (xhr) {
|
||||
xhr.withCredentials = false;
|
||||
}
|
||||
});
|
||||
currentHls.loadSource(finalUrl);
|
||||
currentHls.attachMedia(vd);
|
||||
currentHls.on(Hls.Events.MANIFEST_PARSED, (_, data) => {
|
||||
currentQualityLevels = data.levels || [];
|
||||
vd.play().catch(() => { });
|
||||
});
|
||||
currentHls.on(Hls.Events.LEVEL_SWITCHED, (_, data) => {
|
||||
selectedQualityIndex = data.level;
|
||||
});
|
||||
|
||||
if (pbg) {
|
||||
currentBgHls = new Hls({
|
||||
xhrSetup: function (xhr) {
|
||||
xhr.withCredentials = false;
|
||||
}
|
||||
});
|
||||
currentBgHls.loadSource(finalUrl);
|
||||
currentBgHls.attachMedia(pbg);
|
||||
currentBgHls.on(Hls.Events.MANIFEST_PARSED, () => {
|
||||
pbg.muted = true;
|
||||
if (!vd.paused) pbg.play().catch(() => { });
|
||||
});
|
||||
}
|
||||
} else if (vd.canPlayType('application/vnd.apple.mpegurl')) {
|
||||
vd.src = finalUrl;
|
||||
vd.play().catch(() => { });
|
||||
if (pbg) {
|
||||
pbg.src = finalUrl;
|
||||
pbg.muted = true;
|
||||
pbg.play().catch(() => { });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
currentQualityLevels = [];
|
||||
vd.src = finalUrl;
|
||||
vd.play().catch(() => { });
|
||||
if (pbg) {
|
||||
pbg.src = finalUrl;
|
||||
pbg.muted = true;
|
||||
pbg.play().catch(() => { });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setPlaybackQuality(levelIndex) {
|
||||
selectedQualityIndex = levelIndex;
|
||||
if (currentHls) {
|
||||
currentHls.currentLevel = levelIndex;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSubtitles(id, s, e) {
|
||||
Array.from(vd.querySelectorAll('track')).forEach(t => t.remove());
|
||||
activeSubtitles = [];
|
||||
|
||||
try {
|
||||
let epQuery = s && e ? `&season=${s}&episode=${e}` : '';
|
||||
const res = await fetch(`${API_BASE}/api/subtitles?tmdbId=${id}${epQuery}`);
|
||||
const data = await res.json();
|
||||
activeSubtitles = data.subtitles || [];
|
||||
|
||||
activeSubtitles.forEach((sub, idx) => {
|
||||
const track = document.createElement('track');
|
||||
track.kind = 'subtitles';
|
||||
track.label = `${sub.label} (${sub.source || 'mirror'})`;
|
||||
track.srclang = sub.label.slice(0, 2).toLowerCase();
|
||||
track.src = buildProxyUrl(sub.file);
|
||||
track.default = idx === 0;
|
||||
vd.appendChild(track);
|
||||
track.track.mode = 'disabled';
|
||||
});
|
||||
updateSubStyles();
|
||||
} catch (_) { }
|
||||
}
|
||||
|
||||
async function loadTimestamps(tmdbId, season = null, episode = null, durationSec = null) {
|
||||
mediaSegments = [];
|
||||
activeSegment = null;
|
||||
$('#pskip').classList.remove('v');
|
||||
try {
|
||||
let q = `${API_BASE}/api/timestamps?tmdb_id=${tmdbId}`;
|
||||
if (season) q += `&season=${season}`;
|
||||
if (episode) q += `&episode=${episode}`;
|
||||
if (durationSec) q += `&duration_ms=${Math.round(durationSec * 1000)}`;
|
||||
|
||||
const res = await fetch(q);
|
||||
const data = await res.json();
|
||||
mediaSegments = data.segments || [];
|
||||
} catch (_) {
|
||||
mediaSegments = [];
|
||||
}
|
||||
}
|
||||
|
||||
function renderSourcesMenu() {
|
||||
let container = $('#pm-src-list');
|
||||
if (!container) return;
|
||||
container.innerHTML = '';
|
||||
|
||||
if (!resolvedStreams.length) {
|
||||
container.innerHTML = '<div style="padding:10px 14px;color:rgba(255,255,255,0.4);font-size:13px;">Probing providers...</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
resolvedStreams.forEach(s => {
|
||||
const grp = document.createElement('div');
|
||||
grp.className = 'src-group';
|
||||
|
||||
const hdr = document.createElement('div');
|
||||
hdr.className = 'src-header f' + (s.provider === selectedSourceKey ? ' on' : '');
|
||||
|
||||
const hasMulti = s.allUrls && s.allUrls.length > 1;
|
||||
const countLabel = hasMulti ? ` (${s.allUrls.length})` : '';
|
||||
const expandIcon = hasMulti ? `<span class="src-expand-btn">${expandedSources.has(s.provider) ? '▲' : '▼'}</span>` : '';
|
||||
|
||||
hdr.innerHTML = `<span>${s.label}${countLabel}</span><div>${expandIcon}${s.provider === selectedSourceKey ? PI.chk : ''}</div>`;
|
||||
hdr._a = () => {
|
||||
userHasManuallySelectedSource = true;
|
||||
if (hasMulti && s.provider === selectedSourceKey) {
|
||||
if (expandedSources.has(s.provider)) expandedSources.delete(s.provider);
|
||||
else expandedSources.add(s.provider);
|
||||
renderSourcesMenu();
|
||||
} else {
|
||||
selectedSourceKey = s.provider;
|
||||
playSpecificStream(s.primaryUrl, s.headers);
|
||||
renderSourcesMenu();
|
||||
}
|
||||
};
|
||||
grp.appendChild(hdr);
|
||||
|
||||
if (hasMulti) {
|
||||
const sub = document.createElement('div');
|
||||
sub.className = 'src-sublist' + (expandedSources.has(s.provider) ? ' open' : '');
|
||||
|
||||
s.allUrls.forEach((u, i) => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'src-subitem f' + (u.url === selectedStreamUrl ? ' on' : '');
|
||||
item.innerHTML = `<span>${u.quality || 'Stream ' + (i + 1)}</span>${u.url === selectedStreamUrl ? PI.chk : ''}`;
|
||||
item._a = () => {
|
||||
userHasManuallySelectedSource = true;
|
||||
selectedSourceKey = s.provider;
|
||||
playSpecificStream(u.url, s.headers);
|
||||
renderSourcesMenu();
|
||||
};
|
||||
sub.appendChild(item);
|
||||
});
|
||||
grp.appendChild(sub);
|
||||
}
|
||||
|
||||
container.appendChild(grp);
|
||||
});
|
||||
}
|
||||
|
||||
function openMenu(k) {
|
||||
if (pmK === k) {
|
||||
closeMenu(1);
|
||||
return;
|
||||
}
|
||||
closeEps();
|
||||
pm.innerHTML = '';
|
||||
pmK = k;
|
||||
pm.classList.add('open');
|
||||
let first = null;
|
||||
|
||||
const h = t => {
|
||||
const e = document.createElement('h4');
|
||||
e.textContent = t;
|
||||
pm.appendChild(e);
|
||||
};
|
||||
|
||||
const r = (l, on, fn) => {
|
||||
const e = mkf('pi', `<span>${l}</span>${on ? PI.chk : ''}`, () => {
|
||||
fn();
|
||||
closeMenu(1);
|
||||
});
|
||||
pm.appendChild(e);
|
||||
if (on && !first) first = e;
|
||||
return e;
|
||||
};
|
||||
|
||||
if (k === 'qlt') {
|
||||
h('Stream Quality');
|
||||
if (currentHls && currentQualityLevels.length > 0) {
|
||||
r('Auto', selectedQualityIndex === -1, () => setPlaybackQuality(-1));
|
||||
currentQualityLevels.forEach((lvl, idx) => {
|
||||
const label = lvl.height ? `${lvl.height}p` : `${Math.round(lvl.bitrate / 1000)} kbps`;
|
||||
r(label, selectedQualityIndex === idx, () => setPlaybackQuality(idx));
|
||||
});
|
||||
} else {
|
||||
const activeProv = resolvedStreams.find(s => s.provider === selectedSourceKey);
|
||||
if (activeProv && activeProv.allUrls.length > 1) {
|
||||
activeProv.allUrls.forEach((u, idx) => {
|
||||
const label = u.quality || `Stream ${idx + 1}`;
|
||||
r(label, u.url === selectedStreamUrl, () => {
|
||||
userHasManuallySelectedSource = true;
|
||||
playSpecificStream(u.url, activeProv.headers);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
r('Auto', true, () => { });
|
||||
}
|
||||
}
|
||||
} else if (k === 'cc') {
|
||||
h('Subtitles');
|
||||
const T = [...vd.textTracks];
|
||||
r('Off', !T.some(t => t.mode === 'showing'), () => {
|
||||
T.forEach(t => t.mode = 'disabled');
|
||||
});
|
||||
T.forEach(t => r(t.label, t.mode === 'showing', () => {
|
||||
T.forEach(u => u.mode = u === t ? 'showing' : 'disabled');
|
||||
updateSubStyles();
|
||||
}));
|
||||
|
||||
h('Subtitle Size');
|
||||
[18, 22, 26, 32].forEach(sz => {
|
||||
r(`${sz}px`, subSettings.size === sz, () => {
|
||||
subSettings.size = sz;
|
||||
updateSubStyles();
|
||||
});
|
||||
});
|
||||
|
||||
h('Subtitle Color');
|
||||
[['White', '#ffffff'], ['Yellow', '#ffd700'], ['Cyan', '#00ffff']].forEach(([col, code]) => {
|
||||
r(col, subSettings.color === code, () => {
|
||||
subSettings.color = code;
|
||||
updateSubStyles();
|
||||
});
|
||||
});
|
||||
|
||||
h('Subtitle Sync');
|
||||
const syncRow = document.createElement('div');
|
||||
syncRow.className = 'sub-ctrl-row';
|
||||
syncRow.innerHTML = `<span>Timing Offset</span><div class="sub-btn-group"><button id="sub-min">-0.5s</button><button id="sub-plus">+0.5s</button></div>`;
|
||||
pm.appendChild(syncRow);
|
||||
$('#sub-min').onclick = (ev) => {
|
||||
ev.stopPropagation();
|
||||
applySubOffset(-0.5);
|
||||
};
|
||||
$('#sub-plus').onclick = (ev) => {
|
||||
ev.stopPropagation();
|
||||
applySubOffset(0.5);
|
||||
};
|
||||
} else if (k === 'vol') {
|
||||
h('Volume');
|
||||
const w = document.createElement('div');
|
||||
w.className = 'vrow';
|
||||
w.innerHTML = '<div class="pic f" id="vm"></div><div class="vs f" id="vsl"><i></i></div><span id="vpc"></span>';
|
||||
pm.appendChild(w);
|
||||
const vm = $('#vm');
|
||||
const vs = $('#vsl');
|
||||
const vu = () => {
|
||||
const v = vd.muted ? 0 : vd.volume;
|
||||
vs.firstChild.style.width = v * 100 + '%';
|
||||
$('#vpc').textContent = Math.round(v * 100);
|
||||
vm.innerHTML = $('#pvol').innerHTML = v ? PI.vol : PI.mute;
|
||||
};
|
||||
vm._a = () => {
|
||||
vd.muted = !vd.muted;
|
||||
vu();
|
||||
};
|
||||
vs._a = () => { };
|
||||
vs._v = vu;
|
||||
vs.addEventListener('click', e => {
|
||||
const b = vs.getBoundingClientRect();
|
||||
vd.muted = false;
|
||||
vd.volume = Math.min(1, Math.max(0, (e.clientX - b.left) / b.width));
|
||||
vu();
|
||||
});
|
||||
vu();
|
||||
first = vs;
|
||||
} else if (k === 'set') {
|
||||
h('Available Sources');
|
||||
const srcContainer = document.createElement('div');
|
||||
srcContainer.id = 'pm-src-list';
|
||||
pm.appendChild(srcContainer);
|
||||
renderSourcesMenu();
|
||||
|
||||
h('Playback Speed');
|
||||
[.5, 1, 1.25, 1.5, 2].forEach(v => r(v === 1 ? 'Normal' : v + '\u00d7', vd.playbackRate === v, () => vd.playbackRate = v));
|
||||
|
||||
h('Aspect Ratio');
|
||||
r('Fit Screen', !pl.classList.contains('cover'), () => {
|
||||
pl.classList.remove('cover');
|
||||
if (pbg) {
|
||||
pbg.currentTime = vd.currentTime;
|
||||
if (!vd.paused) pbg.play().catch(() => { });
|
||||
}
|
||||
});
|
||||
r('Fill Screen', pl.classList.contains('cover'), () => {
|
||||
pl.classList.add('cover');
|
||||
if (pbg) pbg.pause();
|
||||
});
|
||||
} else {
|
||||
h('Library');
|
||||
r('Watch Later', has('wl', plItem), () => toggleLib('wl', plItem));
|
||||
r('Favorites', has('fav', plItem), () => toggleLib('fav', plItem));
|
||||
}
|
||||
|
||||
showCtl();
|
||||
setFocus(first || pm.querySelector('.f'));
|
||||
}
|
||||
|
||||
async function openPlayer(it, ep, ctx) {
|
||||
if (!it) return;
|
||||
plPrev = cur;
|
||||
plItem = it;
|
||||
plCtx = ctx || null;
|
||||
userHasManuallySelectedSource = false;
|
||||
$('#peps').style.display = plCtx ? '' : 'none';
|
||||
pl.classList.remove('eps');
|
||||
$('#pnext').classList.remove('v');
|
||||
$('#pt').textContent = it.title || it.name || '';
|
||||
$('#ps').textContent = ep || [yr(it), it._t === 'tv' ? 'Series' : 'Movie'].filter(Boolean).join(' \u2022 ');
|
||||
$('#pav').src = it.poster_path ? I + 'w185' + it.poster_path : PH;
|
||||
vd.poster = it.backdrop_path ? I + 'w1280' + it.backdrop_path : '';
|
||||
|
||||
pl.classList.add('show');
|
||||
showCtl();
|
||||
pico();
|
||||
setFocus($('#pmid'));
|
||||
|
||||
if (plCtx) {
|
||||
const epData = plCtx.eps[plCtx.i];
|
||||
startConcurrentDiscovery(it, plCtx.s, epData.episode_number);
|
||||
} else {
|
||||
startConcurrentDiscovery(it);
|
||||
}
|
||||
}
|
||||
|
||||
function closePlayer() {
|
||||
closeMenu();
|
||||
closeEps();
|
||||
if (currentHls) {
|
||||
currentHls.destroy();
|
||||
currentHls = null;
|
||||
}
|
||||
if (currentBgHls) {
|
||||
currentBgHls.destroy();
|
||||
currentBgHls = null;
|
||||
}
|
||||
plCtx = null;
|
||||
vd.pause();
|
||||
vd.removeAttribute('src');
|
||||
vd.load();
|
||||
if (pbg) {
|
||||
pbg.pause();
|
||||
pbg.removeAttribute('src');
|
||||
pbg.load();
|
||||
}
|
||||
if (document.fullscreenElement) document.exitFullscreen();
|
||||
pl.classList.remove('show', 'ui');
|
||||
if (plPrev && document.body.contains(plPrev)) setFocus(plPrev);
|
||||
}
|
||||
|
||||
$('#pback')._a = closePlayer;
|
||||
$('#pmid')._a = tgl;
|
||||
|
||||
$('#pskip')._a = () => {
|
||||
if (activeSegment) {
|
||||
const target = activeSegment.end != null ? activeSegment.end + 0.5 : (vd.duration || vd.currentTime + 30);
|
||||
vd.currentTime = target;
|
||||
$('#pskip').classList.remove('v');
|
||||
activeSegment = null;
|
||||
} else {
|
||||
vd.currentTime += 85;
|
||||
}
|
||||
};
|
||||
|
||||
$('#pqlt')._a = () => openMenu('qlt');
|
||||
$('#pcc')._a = () => openMenu('cc');
|
||||
$('#pvol')._a = () => openMenu('vol');
|
||||
$('#pset')._a = () => openMenu('set');
|
||||
$('#pmore')._a = () => openMenu('more');
|
||||
$('#pfs')._a = () => document.fullscreenElement ? document.exitFullscreen() : pl.requestFullscreen && pl.requestFullscreen();
|
||||
|
||||
document.addEventListener('fullscreenchange', () => {
|
||||
$('#pfs').innerHTML = document.fullscreenElement ? PI.fx : PI.fs;
|
||||
});
|
||||
|
||||
const sp = e => {
|
||||
const b = sk.getBoundingClientRect();
|
||||
vd.currentTime = Math.min(1, Math.max(0, (e.clientX - b.left) / b.width)) * (vd.duration || 0);
|
||||
};
|
||||
|
||||
sk.addEventListener('pointerdown', e => {
|
||||
drag = 1;
|
||||
sk._m = 1;
|
||||
sk.setPointerCapture(e.pointerId);
|
||||
sp(e);
|
||||
});
|
||||
|
||||
sk.addEventListener('pointermove', e => {
|
||||
if (drag) sp(e);
|
||||
});
|
||||
|
||||
sk.addEventListener('pointerup', () => drag = 0);
|
||||
|
||||
sk._a = () => {
|
||||
if (sk._m) {
|
||||
sk._m = 0;
|
||||
return;
|
||||
}
|
||||
tgl();
|
||||
};
|
||||
|
||||
pl.querySelectorAll('.f').forEach(e => {
|
||||
if (!e._a) e._a = () => { };
|
||||
});
|
||||
|
||||
vd.addEventListener('click', () => {
|
||||
if (pmK) closeMenu();
|
||||
else if (pl.classList.contains('eps')) closeEps();
|
||||
else tgl();
|
||||
showCtl();
|
||||
});
|
||||
|
||||
vd.addEventListener('play', () => {
|
||||
if (!pl.classList.contains('cover') && pbg) {
|
||||
pbg.muted = true;
|
||||
pbg.currentTime = vd.currentTime;
|
||||
pbg.play().catch(() => { });
|
||||
}
|
||||
});
|
||||
|
||||
vd.addEventListener('pause', () => {
|
||||
if (pbg) pbg.pause();
|
||||
});
|
||||
|
||||
vd.addEventListener('seeking', () => {
|
||||
if (pbg) pbg.currentTime = vd.currentTime;
|
||||
});
|
||||
|
||||
vd.addEventListener('seeked', () => {
|
||||
if (pbg) pbg.currentTime = vd.currentTime;
|
||||
});
|
||||
|
||||
vd.addEventListener('ratechange', () => {
|
||||
if (pbg) pbg.playbackRate = vd.playbackRate;
|
||||
});
|
||||
|
||||
vd.addEventListener('durationchange', () => {
|
||||
$('#pdur').textContent = fmt(vd.duration);
|
||||
if (vd.duration && plItem) {
|
||||
const c = plCtx;
|
||||
loadTimestamps(plItem.id, c ? c.s : null, c ? c.eps[c.i].episode_number : null, vd.duration);
|
||||
}
|
||||
});
|
||||
|
||||
vd.addEventListener('timeupdate', () => {
|
||||
const d = vd.duration || 0;
|
||||
const t = vd.currentTime;
|
||||
const p = d ? (t / d) * 100 : 0;
|
||||
|
||||
$('#pfill').style.width = p + '%';
|
||||
$('#phd').style.left = p + '%';
|
||||
$('#pcur').textContent = fmt(t);
|
||||
|
||||
if (d && vd.buffered.length) {
|
||||
$('#pbuf').style.width = (vd.buffered.end(vd.buffered.length - 1) / d * 100) + '%';
|
||||
}
|
||||
|
||||
if (!pl.classList.contains('cover') && pbg && Math.abs(pbg.currentTime - t) > 0.35) {
|
||||
pbg.currentTime = t;
|
||||
}
|
||||
|
||||
const currentSeg = mediaSegments.find(seg => {
|
||||
const afterStart = t >= seg.start;
|
||||
const beforeEnd = seg.end == null ? (d ? t < d - 5 : true) : t < seg.end;
|
||||
return afterStart && beforeEnd;
|
||||
});
|
||||
|
||||
if (currentSeg) {
|
||||
activeSegment = currentSeg;
|
||||
$('#pskip').textContent = currentSeg.label;
|
||||
$('#pskip').classList.add('v');
|
||||
} else {
|
||||
activeSegment = null;
|
||||
$('#pskip').classList.remove('v');
|
||||
}
|
||||
});
|
||||
|
||||
['play', 'pause', 'ended'].forEach(n => vd.addEventListener(n, pico));
|
||||
['loadstart', 'waiting', 'seeking'].forEach(n => vd.addEventListener(n, () => pl.classList.add('buf')));
|
||||
['playing', 'canplay', 'seeked', 'error'].forEach(n => vd.addEventListener(n, () => pl.classList.remove('buf')));
|
||||
|
||||
const hasNext = () => !!plCtx && (plCtx.i + 1 < plCtx.eps.length || plCtx.seasons.some(x => x.season_number > plCtx.s));
|
||||
const getSeason = async s => plCtx.cache[s] || (plCtx.cache[s] = (await api(`/tv/${plCtx.it.id}/season/${s}`)).episodes || []);
|
||||
|
||||
async function playEp(s, i) {
|
||||
const c = plCtx;
|
||||
if (!c) return;
|
||||
const eps = await getSeason(s);
|
||||
const p = eps[i];
|
||||
if (!p || plCtx !== c) return;
|
||||
c.s = s;
|
||||
c.i = i;
|
||||
c.eps = eps;
|
||||
$('#ps').textContent = `S${s} E${p.episode_number} \u2022 ${p.name || ''}`;
|
||||
$('#pnext').classList.remove('v');
|
||||
closeEps();
|
||||
showCtl();
|
||||
setFocus($('#pmid'));
|
||||
userHasManuallySelectedSource = false;
|
||||
startConcurrentDiscovery(c.it, s, p.episode_number);
|
||||
}
|
||||
|
||||
function nextEp() {
|
||||
const c = plCtx;
|
||||
if (!hasNext()) return;
|
||||
if (c.i + 1 < c.eps.length) {
|
||||
playEp(c.s, c.i + 1);
|
||||
} else {
|
||||
playEp(c.seasons.find(x => x.season_number > c.s).season_number, 0);
|
||||
}
|
||||
}
|
||||
|
||||
function closeEps(rf) {
|
||||
if (!pl.classList.contains('eps')) return;
|
||||
pl.classList.remove('eps');
|
||||
if (rf) setFocus($('#peps'));
|
||||
}
|
||||
|
||||
function openEps() {
|
||||
const c = plCtx;
|
||||
if (!c) return;
|
||||
closeMenu();
|
||||
$('#peT').innerHTML = `Episodes<small>${esc(c.it.title || c.it.name || '')}</small>`;
|
||||
const sr = $('#pseas');
|
||||
sr.innerHTML = '';
|
||||
c.seasons.forEach(x => {
|
||||
sr.appendChild(mkf('chip', esc(x.name || 'Season ' + x.season_number), () => showSeason(x.season_number)));
|
||||
});
|
||||
pl.classList.add('eps');
|
||||
showCtl();
|
||||
showSeason(c.s, true);
|
||||
}
|
||||
|
||||
async function showSeason(sn, focus) {
|
||||
const c = plCtx;
|
||||
const car = $('#pcar');
|
||||
if (!c) return;
|
||||
viewS = sn;
|
||||
[...$('#pseas').children].forEach((e, i) => e.classList.toggle('act', c.seasons[i].season_number === sn));
|
||||
car.innerHTML = SK.pep(8);
|
||||
const eps = await getSeason(sn);
|
||||
if (viewS !== sn || plCtx !== c) return;
|
||||
car.innerHTML = '';
|
||||
eps.forEach((p, i) => {
|
||||
const n = p.episode_number;
|
||||
const cur = c.s === sn && c.i === i;
|
||||
const img = p.still_path ? I + 'w300' + p.still_path : (c.it.backdrop_path ? I + 'w300' + c.it.backdrop_path : PH);
|
||||
car.appendChild(mkf('pe' + (cur ? ' cur' : ''), `<div class="et"><img loading="lazy" src="${img}" alt=""><i>${n}</i>${cur ? '<span class="eq"><u></u><u></u><u></u></span><span class="pg"><u></u></span>' : ''}</div><span class="pn">${esc(p.name || 'Episode ' + n)}</span><small>${p.runtime ? p.runtime + 'm' : 'Episode ' + n}</small>`, () => playEp(sn, i)));
|
||||
});
|
||||
const t = car.querySelector('.cur') || car.firstChild;
|
||||
if (focus && t) setFocus(t);
|
||||
}
|
||||
|
||||
$('#pep').innerHTML = `<div class="peh"><h3 id="peT"></h3><div class="row" id="pseas"></div><div class="pic f" id="pepx">${PI.x}</div></div><div class="row pcar" id="pcar"></div>`;
|
||||
$('#pnext').innerHTML = 'Next Episode' + pi('<path d="M6 5.5v13l9-6.5z"/><path d="M18 5.5v13"/>');
|
||||
$('#pnext')._a = nextEp;
|
||||
$('#pepx')._a = () => closeEps(1);
|
||||
$('#peps')._a = () => pl.classList.contains('eps') ? closeEps(1) : openEps();
|
||||
|
||||
vd.addEventListener('timeupdate', () => {
|
||||
const d = vd.duration || 0;
|
||||
const r = d - vd.currentTime;
|
||||
$('#pnext').classList.toggle('v', !!d && hasNext() && r <= 300 && r > 0);
|
||||
$('#pep').style.setProperty('--p', d ? (vd.currentTime / d) * 100 + '%' : '0%');
|
||||
});
|
||||
|
||||
vd.addEventListener('ended', () => {
|
||||
if (hasNext()) nextEp();
|
||||
});
|
||||
70
www/js/search.js
Normal file
@@ -0,0 +1,70 @@
|
||||
let sqv = '', bgT, stab = 0, SD = { q: '', all: [], mv: [], tv: [] };
|
||||
const SVS = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>';
|
||||
const sdraw = () => { const t = $('#stext'); t.textContent = sqv || 'Search'; t.classList.toggle('q', !!sqv) };
|
||||
function setQ(v) { sqv = v; sdraw(); clearTimeout(st); st = setTimeout(() => fillSearch(sqv.trim()), 300) }
|
||||
function sbgf(it) { clearTimeout(bgT); bgT = setTimeout(() => xf(bgb, I + 'w780' + (it.backdrop_path || it.poster_path)), 120) }
|
||||
async function fillSearch(q) {
|
||||
const my = ++sgen; skelSearch();
|
||||
let all, mv, tv;
|
||||
const ok = x => x.poster_path && (x.media_type === 'movie' || x.media_type === 'tv');
|
||||
if (q) {
|
||||
const d = await api('/search/multi', `&query=${encodeURIComponent(q)}&include_adult=false`);
|
||||
if (my !== sgen) return;
|
||||
all = (d.results || []).filter(ok); mv = all.filter(x => x.media_type === 'movie'); tv = all.filter(x => x.media_type === 'tv')
|
||||
}
|
||||
else {
|
||||
const [a, b] = await Promise.all([api('/trending/movie/week'), api('/trending/tv/week')]);
|
||||
if (my !== sgen) return;
|
||||
mv = (a.results || []).filter(x => x.poster_path).map(x => ({ ...x, media_type: 'movie' }));
|
||||
tv = (b.results || []).filter(x => x.poster_path).map(x => ({ ...x, media_type: 'tv' })); all = []
|
||||
}
|
||||
SD = { q, all, mv, tv }; drawSearch()
|
||||
}
|
||||
function tile(x) {
|
||||
x._t = x.media_type;
|
||||
const e = mkf('sc', `<div class="po"><img src="${I}w342${x.poster_path}" alt=""></div><b>${esc(x.title || x.name || '')}</b><small><span>★ ${(x.vote_average || 0).toFixed(1)}</span><span>${yr(x)}</span><span>${x.media_type === 'tv' ? 'Show' : 'Movie'}</span></small>`, () => openDetail(x));
|
||||
e._bg = x; return e
|
||||
}
|
||||
function drawSearch() {
|
||||
const { q, all, mv, tv } = SD;
|
||||
chips.innerHTML = '';
|
||||
const names = [...new Set((q ? all : [...mv, ...tv]).map(x => x.title || x.name))].slice(0, 9);
|
||||
if (q) chips.appendChild(mkf('chip', SVS + `"${esc(q)}"`, () => { }));
|
||||
names.forEach(n => chips.appendChild(mkf('chip', esc(n), () => setQ(n))));
|
||||
sres.innerHTML = '';
|
||||
const L = stab === 1 ? [['Movies', mv]] : stab === 2 ? [['TV Shows', tv]] : q ? [['Top Matches', all.slice(0, 10)], ['Movies', mv], ['TV Shows', tv]] : [['Trending Movies', mv], ['Trending TV Shows', tv]];
|
||||
let n = 0;
|
||||
L.forEach(([h, a]) => { if (!a.length) return; n++; const sec = document.createElement('section'); sec.className = 'ss'; sec.innerHTML = `<h3>${h}</h3><div class="row ch"></div>`; sec.lastChild.append(...a.map(tile)); sres.appendChild(sec) });
|
||||
if (!n) sres.innerHTML = `<p class="none">No results${q ? ` for \u201c${esc(q)}\u201d` : ''}</p>`;
|
||||
[...tabs.children].forEach((t, i) => t.classList.toggle('act', i === stab));
|
||||
if (cur && !document.body.contains(cur)) setFocus(chips.children[1] || chips.firstChild || kb.children[2])
|
||||
}
|
||||
['Top Results', 'Movies', 'TV Shows'].forEach((t, i) => tabs.appendChild(mkf('tab', t, () => { stab = i; drawSearch() })));
|
||||
const KL = 'abcdefghijklmnopqrstuvwxyz'.split(''), KN = "1234567890.-'&:".split('');
|
||||
let num = false;
|
||||
function buildKb() {
|
||||
kb.innerHTML = '';
|
||||
const mk = (label, cls, fn) => kb.appendChild(mkf('k ' + cls, label, () => { fn(); sr.scrollTo({ top: 0, behavior: 'smooth' }) }));
|
||||
mk(num ? 'ABC' : '123', 'w', () => { num = !num; buildKb(); setFocus(kb.firstChild) });
|
||||
mk('SPACE', 'w', () => { if (sqv && !sqv.endsWith(' ')) setQ(sqv + ' ') });
|
||||
(num ? KN : KL).forEach(c => mk(c, '', () => setQ(sqv + c)));
|
||||
mk('<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M9 5h11a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H9l-6-7z"/><path d="m12 9 5 5m0-5-5 5"/></svg>', 'd', () => setQ(sqv.slice(0, -1)))
|
||||
}
|
||||
buildKb();
|
||||
function openSearch() {
|
||||
setAct('search'); document.body.classList.add('srch'); sr.classList.add('show'); side.classList.remove('open'); sr.scrollTop = 0;
|
||||
if (!sres.children.length) fillSearch('');
|
||||
setFocus(kb.children[2])
|
||||
}
|
||||
function closeSearch(silent) {
|
||||
if (!sr.classList.contains('show')) return;
|
||||
sr.classList.remove('show'); document.body.classList.remove('srch');
|
||||
heroIt = null; bgb._u = null;
|
||||
if (!silent) { setAct(mode); setFocus(document.querySelector('#rows .card')) }
|
||||
}
|
||||
|
||||
|
||||
function skelSearch() {
|
||||
chips.innerHTML = SK.chips(7);
|
||||
sres.innerHTML = [0, 1].map(() => `<section class="ss"><i class="sb h3s"></i><div class="row ch">${SK.tiles(9)}</div></section>`).join('')
|
||||
}
|
||||
16
www/js/sidebar.js
Normal file
@@ -0,0 +1,16 @@
|
||||
const NAV = [
|
||||
['search', 'Search', ic('<circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/>')],
|
||||
['discover', 'Discovery', ic('<circle cx="12" cy="12" r="9"/><path d="m15.5 8.5-2 5-5 2 2-5z"/>')],
|
||||
['movie', 'Movies', ic('<rect x="3" y="3" width="18" height="18" rx="2"/><path d="M7 3v18M17 3v18M3 8h4M3 12h4M3 16h4M17 8h4M17 12h4M17 16h4"/>')],
|
||||
['tv', 'TV Shows', ic('<rect x="3" y="7" width="18" height="13" rx="2"/><path d="m8 3 4 4 4-4"/>')],
|
||||
['wl', 'Watch Later', ic(P_WL)],
|
||||
['fav', 'Favorites', ic(P_FV)]
|
||||
];
|
||||
|
||||
NAV.forEach(([k, n, s]) => { const e = document.createElement('div'); e.className = 'ni f'; e.tabIndex = -1; e.innerHTML = s + `<span>${n}</span>`; e._a = () => go(k); NAVEL[k] = e; $('#nav').appendChild(e) });
|
||||
syncNav();
|
||||
function setAct(k) { Object.entries(NAVEL).forEach(([n, e]) => e.classList.toggle('act', n === k)); navAct = NAVEL[k] }
|
||||
const mark = $('#mark'); mark.onerror = () => { mark.onerror = null; mark.src = PH }; mark.src = LOGO;
|
||||
|
||||
|
||||
function syncNav() { ['wl', 'fav'].forEach(k => { if (NAVEL[k]) NAVEL[k].style.display = (LIB[k].length || libK === k) ? '' : 'none' }) }
|
||||
13
www/js/skeleton.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const SK = {
|
||||
cards: n => '<div class="card sl"></div>'.repeat(n),
|
||||
tiles: n => '<div class="sc sl"><div class="po"></div><i class="sb"></i><i class="sb s"></i></div>'.repeat(n),
|
||||
chips: n => Array.from({ length: n }, (_, i) => `<div class="chip sl" style="width:${90 + (i * 37) % 80}px"></div>`).join(''),
|
||||
pep: n => '<div class="pe sl"><div class="et"></div><i class="sb" style="width:70%;height:14px;margin-top:11px"></i><i class="sb" style="width:35%;height:11px;margin-top:6px"></i></div>'.repeat(n),
|
||||
eps: n => '<div class="ep sl"><div class="et"></div><i class="sb" style="width:70%;height:14px;margin-top:12px"></i><i class="sb" style="width:40%;height:11px;margin-top:6px"></i></div>'.repeat(n),
|
||||
rows: n => Array.from({ length: n }, () => '<section class="sec"><i class="sb h2"></i><div class="row">' + SK.cards(14) + '</div></section>').join(''),
|
||||
detail: it => {
|
||||
const bp = it.backdrop_path || it.poster_path || '';
|
||||
return `<div class="dbl" style="background-image:url(${I}w780${bp})"></div><div class="dw"><div class="dimg" style="background-image:url(${I}w1280${bp})"></div><div class="dc"><div class="dh dsk"><i class="sb t"></i><i class="sb m"></i><div class="hb"><i class="sb b"></i><i class="sb c"></i></div><i class="sb l"></i><i class="sb l"></i><i class="sb l x"></i></div>` +
|
||||
`<div class="ds"><i class="sb h3"></i><div class="row">${'<div class="trk"></div>'.repeat(4)}</div></div><div class="ds"><i class="sb h3"></i><div class="row">${SK.cards(9)}</div></div></div></div>`
|
||||
}
|
||||
};
|
||||