x1ongzhu 1 год назад
Родитель
Сommit
28db852143
68 измененных файлов с 1428 добавлено и 75 удалено
  1. 101 0
      android/.gitignore
  2. 3 0
      android/.idea/.gitignore
  3. 6 0
      android/.idea/compiler.xml
  4. 10 0
      android/.idea/deploymentTargetDropDown.xml
  5. 10 0
      android/.idea/migrations.xml
  6. 10 0
      android/.idea/misc.xml
  7. 2 0
      android/app/.gitignore
  8. 54 0
      android/app/build.gradle
  9. 22 0
      android/app/capacitor.build.gradle
  10. 21 0
      android/app/proguard-rules.pro
  11. 26 0
      android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java
  12. 41 0
      android/app/src/main/AndroidManifest.xml
  13. 5 0
      android/app/src/main/java/io/freeshort/freeshort/MainActivity.java
  14. BIN
      android/app/src/main/res/drawable-land-hdpi/splash.png
  15. BIN
      android/app/src/main/res/drawable-land-mdpi/splash.png
  16. BIN
      android/app/src/main/res/drawable-land-xhdpi/splash.png
  17. BIN
      android/app/src/main/res/drawable-land-xxhdpi/splash.png
  18. BIN
      android/app/src/main/res/drawable-land-xxxhdpi/splash.png
  19. BIN
      android/app/src/main/res/drawable-port-hdpi/splash.png
  20. BIN
      android/app/src/main/res/drawable-port-mdpi/splash.png
  21. BIN
      android/app/src/main/res/drawable-port-xhdpi/splash.png
  22. BIN
      android/app/src/main/res/drawable-port-xxhdpi/splash.png
  23. BIN
      android/app/src/main/res/drawable-port-xxxhdpi/splash.png
  24. 34 0
      android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
  25. 170 0
      android/app/src/main/res/drawable/ic_launcher_background.xml
  26. BIN
      android/app/src/main/res/drawable/splash.png
  27. 12 0
      android/app/src/main/res/layout/activity_main.xml
  28. 5 0
      android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
  29. 5 0
      android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
  30. BIN
      android/app/src/main/res/mipmap-hdpi/ic_launcher.png
  31. BIN
      android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
  32. BIN
      android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
  33. BIN
      android/app/src/main/res/mipmap-mdpi/ic_launcher.png
  34. BIN
      android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
  35. BIN
      android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
  36. BIN
      android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
  37. BIN
      android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
  38. BIN
      android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
  39. BIN
      android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
  40. BIN
      android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
  41. BIN
      android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
  42. BIN
      android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
  43. BIN
      android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
  44. BIN
      android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
  45. 4 0
      android/app/src/main/res/values/ic_launcher_background.xml
  46. 7 0
      android/app/src/main/res/values/strings.xml
  47. 22 0
      android/app/src/main/res/values/styles.xml
  48. 5 0
      android/app/src/main/res/xml/file_paths.xml
  49. 18 0
      android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java
  50. 29 0
      android/build.gradle
  51. 15 0
      android/capacitor.settings.gradle
  52. 22 0
      android/gradle.properties
  53. BIN
      android/gradle/wrapper/gradle-wrapper.jar
  54. 6 0
      android/gradle/wrapper/gradle-wrapper.properties
  55. 244 0
      android/gradlew
  56. 92 0
      android/gradlew.bat
  57. 5 0
      android/settings.gradle
  58. 16 0
      android/variables.gradle
  59. 9 7
      capacitor.config.ts
  60. 2 0
      package.json
  61. 294 35
      src/components/PlayView.vue
  62. 1 1
      src/main.ts
  63. 18 12
      src/views/HomeTab.vue
  64. 64 17
      src/views/SeriesView.vue
  65. 2 2
      src/views/TabsPage.vue
  66. 5 1
      tailwind.config.js
  67. 1 0
      vite.config.ts
  68. 10 0
      yarn.lock

+ 101 - 0
android/.gitignore

@@ -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

+ 3 - 0
android/.idea/.gitignore

@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml

+ 6 - 0
android/.idea/compiler.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="CompilerConfiguration">
+    <bytecodeTargetLevel target="17" />
+  </component>
+</project>

+ 10 - 0
android/.idea/deploymentTargetDropDown.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="deploymentTargetDropDown">
+    <value>
+      <entry key="app">
+        <State />
+      </entry>
+    </value>
+  </component>
+</project>

+ 10 - 0
android/.idea/migrations.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectMigrations">
+    <option name="MigrateToGradleLocalJavaHome">
+      <set>
+        <option value="$PROJECT_DIR$" />
+      </set>
+    </option>
+  </component>
+</project>

+ 10 - 0
android/.idea/misc.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ExternalStorageConfigurationManager" enabled="true" />
+  <component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
+    <output url="file://$PROJECT_DIR$/build/classes" />
+  </component>
+  <component name="ProjectType">
+    <option name="id" value="Android" />
+  </component>
+</project>

+ 2 - 0
android/app/.gitignore

@@ -0,0 +1,2 @@
+/build/*
+!/build/.npmkeep

+ 54 - 0
android/app/build.gradle

@@ -0,0 +1,54 @@
+apply plugin: 'com.android.application'
+
+android {
+    namespace "io.freeshort.freeshort"
+    compileSdkVersion rootProject.ext.compileSdkVersion
+    defaultConfig {
+        applicationId "io.freeshort.freeshort"
+        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")
+}

+ 22 - 0
android/app/capacitor.build.gradle

@@ -0,0 +1,22 @@
+// 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 {
+    implementation project(':capacitor-app')
+    implementation project(':capacitor-haptics')
+    implementation project(':capacitor-keyboard')
+    implementation project(':capacitor-status-bar')
+
+}
+
+
+if (hasProperty('postBuildExtras')) {
+  postBuildExtras()
+}

+ 21 - 0
android/app/proguard-rules.pro

@@ -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

+ 26 - 0
android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java

@@ -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());
+    }
+}

+ 41 - 0
android/app/src/main/AndroidManifest.xml

@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <application
+        android:allowBackup="true"
+        android:icon="@mipmap/ic_launcher"
+        android:label="@string/app_name"
+        android:roundIcon="@mipmap/ic_launcher_round"
+        android:supportsRtl="true"
+        android:theme="@style/AppTheme">
+
+        <activity
+            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
+            android:name=".MainActivity"
+            android:label="@string/title_activity_main"
+            android:theme="@style/AppTheme.NoActionBarLaunch"
+            android:launchMode="singleTask"
+            android:exported="true">
+
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </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>
+
+    <!-- Permissions -->
+
+    <uses-permission android:name="android.permission.INTERNET" />
+</manifest>

+ 5 - 0
android/app/src/main/java/io/freeshort/freeshort/MainActivity.java

@@ -0,0 +1,5 @@
+package io.freeshort.freeshort;
+
+import com.getcapacitor.BridgeActivity;
+
+public class MainActivity extends BridgeActivity {}

BIN
android/app/src/main/res/drawable-land-hdpi/splash.png


BIN
android/app/src/main/res/drawable-land-mdpi/splash.png


BIN
android/app/src/main/res/drawable-land-xhdpi/splash.png


BIN
android/app/src/main/res/drawable-land-xxhdpi/splash.png


BIN
android/app/src/main/res/drawable-land-xxxhdpi/splash.png


BIN
android/app/src/main/res/drawable-port-hdpi/splash.png


BIN
android/app/src/main/res/drawable-port-mdpi/splash.png


BIN
android/app/src/main/res/drawable-port-xhdpi/splash.png


BIN
android/app/src/main/res/drawable-port-xxhdpi/splash.png


BIN
android/app/src/main/res/drawable-port-xxxhdpi/splash.png


+ 34 - 0
android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml

@@ -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 - 0
android/app/src/main/res/drawable/ic_launcher_background.xml

@@ -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


+ 12 - 0
android/app/src/main/res/layout/activity_main.xml

@@ -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>

+ 5 - 0
android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml

@@ -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>

+ 5 - 0
android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml

@@ -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


BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png


BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png


BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png


BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png


BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png


BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png


BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png


BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png


BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png


BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png


BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png


BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png


BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png


BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png


+ 4 - 0
android/app/src/main/res/values/ic_launcher_background.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="ic_launcher_background">#FFFFFF</color>
+</resources>

+ 7 - 0
android/app/src/main/res/values/strings.xml

@@ -0,0 +1,7 @@
+<?xml version='1.0' encoding='utf-8'?>
+<resources>
+    <string name="app_name">FreeShort</string>
+    <string name="title_activity_main">FreeShort</string>
+    <string name="package_name">io.freeshort.freeshort</string>
+    <string name="custom_url_scheme">io.freeshort.freeshort</string>
+</resources>

+ 22 - 0
android/app/src/main/res/values/styles.xml

@@ -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 - 0
android/app/src/main/res/xml/file_paths.xml

@@ -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>

+ 18 - 0
android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java

@@ -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 - 0
android/build.gradle

@@ -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.0.0'
+        classpath 'com.google.gms:google-services:4.3.15'
+
+        // 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
+}

+ 15 - 0
android/capacitor.settings.gradle

@@ -0,0 +1,15 @@
+// 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')
+
+include ':capacitor-app'
+project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android')
+
+include ':capacitor-haptics'
+project(':capacitor-haptics').projectDir = new File('../node_modules/@capacitor/haptics/android')
+
+include ':capacitor-keyboard'
+project(':capacitor-keyboard').projectDir = new File('../node_modules/@capacitor/keyboard/android')
+
+include ':capacitor-status-bar'
+project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacitor/status-bar/android')

+ 22 - 0
android/gradle.properties

@@ -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


+ 6 - 0
android/gradle/wrapper/gradle-wrapper.properties

@@ -0,0 +1,6 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-all.zip
+networkTimeout=10000
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists

+ 244 - 0
android/gradlew

@@ -0,0 +1,244 @@
+#!/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
+
+# 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"'
+
+# 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
+    which java >/dev/null 2>&1 || 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
+
+# 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
+
+# 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 - 0
android/gradlew.bat

@@ -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 - 0
android/settings.gradle

@@ -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 - 0
android/variables.gradle

@@ -0,0 +1,16 @@
+ext {
+    minSdkVersion = 22
+    compileSdkVersion = 33
+    targetSdkVersion = 33
+    androidxActivityVersion = '1.7.0'
+    androidxAppCompatVersion = '1.6.1'
+    androidxCoordinatorLayoutVersion = '1.2.0'
+    androidxCoreVersion = '1.10.0'
+    androidxFragmentVersion = '1.5.6'
+    coreSplashScreenVersion = '1.0.0'
+    androidxWebkitVersion = '1.6.1'
+    junitVersion = '4.13.2'
+    androidxJunitVersion = '1.1.5'
+    androidxEspressoCoreVersion = '3.5.1'
+    cordovaAndroidVersion = '10.1.1'
+}

+ 9 - 7
capacitor.config.ts

@@ -1,12 +1,14 @@
-import { CapacitorConfig } from '@capacitor/cli';
+import { CapacitorConfig } from "@capacitor/cli";
 
 const config: CapacitorConfig = {
-  appId: 'io.ionic.starter',
-  appName: 'LibreShort',
-  webDir: 'dist',
-  server: {
-    androidScheme: 'https'
-  }
+    appId: "io.freeshort.freeshort",
+    appName: "FreeShort",
+    webDir: "dist",
+    server: {
+        androidScheme: "https",
+        url: "http://192.168.6.215:4173",
+    },
+    android: {},
 };
 
 export default config;

+ 2 - 0
package.json

@@ -12,9 +12,11 @@
     "lint": "eslint ."
   },
   "dependencies": {
+    "@capacitor/android": "^5.7.2",
     "@capacitor/app": "5.0.7",
     "@capacitor/core": "5.7.2",
     "@capacitor/haptics": "5.0.7",
+    "@capacitor/ios": "^5.7.2",
     "@capacitor/keyboard": "5.0.8",
     "@capacitor/status-bar": "5.0.7",
     "@ionic/vue": "^7.0.0",

+ 294 - 35
src/components/PlayView.vue

@@ -1,27 +1,68 @@
 <template>
     <div class="bg-neutral-900 w-full h-full relative">
         <video
+            v-show="playUrl"
             ref="video"
             playsinline
-            src="https://zm-shorts.oss-cn-hangzhou.aliyuncs.com/uploads/cltreekiv0003lr8i0p93a5h5.mp4"
-            poster="https://zm-shorts.oss-cn-hangzhou.aliyuncs.com/uploads/cltreg5k20005lr8i8obwfy11.png"
+            :src="playUrl"
+            :poster="series?.cover"
             :controls="false"
             class="object-contain w-full h-full"
             @timeupdate="onTimeUpdate"
+            @play="() => (userPaused = false)"
+            @pause="() => (userPaused = true)"
         ></video>
+        <img
+            v-show="!playUrl && series"
+            :src="series?.cover"
+            class="object-contain w-full h-full"
+        />
+
         <div
             v-show="!draggingData.dragging"
             class="mask-info absolute w-full h-full top-0 left-0"
         >
+            <IonSpinner
+                v-if="!series || !episode"
+                name="crescent"
+                class="absolute left-0 top-0 right-0 bottom-0 m-auto text-lg"
+            />
+            <IonIcon
+                v-if="showPlayBtn"
+                :icon="playCircle"
+                class="absolute left-0 top-0 right-0 bottom-0 m-auto text-6xl opacity-80"
+                @click="onPlay"
+            />
+            <IonIcon
+                v-if="showPauseBtn"
+                :icon="pauseCircle"
+                class="absolute left-0 top-0 right-0 bottom-0 m-auto text-6xl opacity-80"
+            />
+            <div class="tool-bar flex">
+                <div class="px-2 flex items-center" @click="router.back()">
+                    <IonIcon
+                        :icon="chevronBack"
+                        class="text-2xl opacity-80 h-10"
+                    />
+                </div>
+                <div class="flex-1"></div>
+                <div class="px-4 flex items-center">
+                    <IonIcon
+                        :icon="ellipsisHorizontal"
+                        class="text-2xl opacity-80 h-10"
+                    />
+                </div>
+            </div>
             <div
                 class="title-info absolute bottom-0 left-0 right-0 drop-shadow px-4"
+                v-show="series"
             >
                 <div class="pr-20">
                     <div class="text-base line-clamp-1">
-                        Never gonna give you up
+                        {{ series?.title }}
                     </div>
                     <div class="text-sm text-opacity-80 text-white">
-                        Episode 1
+                        Episode {{ episode?.episodeNum }}
                     </div>
                 </div>
                 <div ref="dragTarget" class="py-3">
@@ -42,22 +83,23 @@
                 </div>
                 <div
                     class="dive-into h-[44px] flex items-center text-xs bg-opacity-20 bg-black px-4 rounded"
-                    @click.stop="showEpisodes"
+                    @click.stop="showEpisodesModal = true"
                 >
-                    <div class="flex-1">Dive into the story · 66 Episodes</div>
-                    <img
-                        src="@/assets/icon_into_small.svg"
-                        style="width: 10px; height: auto"
-                    />
+                    <IonIcon :icon="filter" class="text-base" />
+                    <div class="flex-1 ml-2 text-white text-opacity-80">
+                        {{ series?.totalEpisodes }} Episodes
+                    </div>
+                    <IonIcon :icon="chevronForward" class="text-base" />
                 </div>
             </div>
-            <div class="absolute right-4 bottom-32 flex flex-col">
+
+            <div class="absolute right-4 bottom-32 flex flex-col" v-if="series">
                 <div class="btn flex flex-col items-center justify-center">
-                    <img class="w-8 h-8" src="@/assets/icon_save.svg" />
+                    <IonIcon :icon="bookmark" class="text-4xl opacity-80" />
                     <div class="text-xs">Save</div>
                 </div>
                 <div class="btn flex flex-col items-center justify-center mt-4">
-                    <img class="w-8 h-8" src="@/assets/icon_share.svg" />
+                    <IonIcon :icon="arrowRedo" class="text-4xl opacity-80" />
                     <div class="text-xs">Share</div>
                 </div>
             </div>
@@ -71,11 +113,13 @@
             >
                 <span
                     class="inline-block text-center w-16 text-white text-opacity-90"
-                    >{{ toDuration }}</span
-                >/
-                <span class="inline-block text-center w-16">
-                    {{ formatDuration(draggingData.videoDuration) }}</span
                 >
+                    {{ toDuration }}
+                </span>
+                /
+                <span class="inline-block text-center w-16">
+                    {{ formatDuration(draggingData.videoDuration) }}
+                </span>
             </div>
             <div class="progress-bar-large h-2 bg-white bg-opacity-20 rounded">
                 <div
@@ -85,23 +129,153 @@
             </div>
         </div>
     </div>
+
+    <ion-modal
+        ref="modal"
+        class="episodes-modal"
+        :is-open="showEpisodesModal"
+        :initial-breakpoint="1"
+        :breakpoints="[0, 1]"
+        @ionModalDidDismiss="showEpisodesModal = false"
+    >
+        <ion-content>
+            <div class="flex flex-col h-full">
+                <div class="text-lg mt-4 mx-4">{{ series?.title }}</div>
+                <div class="text-sm mt-1 mx-4 text-white text-opacity-80">
+                    {{ series?.totalEpisodes }} Episodes
+                </div>
+                <div class="flex-1 flex flex-wrap overflow-auto mt-4 ml-4 mr-1">
+                    <div
+                        class="pr-3 w-1/4 mb-3"
+                        v-for="(item, n) in episodes || []"
+                        :key="item.id"
+                        @click="chooseEpisode(n)"
+                    >
+                        <div
+                            class="episode-btn h-10 bg-neutral-800 rounded flex items-center justify-center relative"
+                            :class="{ active: item.id === episode?.id }"
+                        >
+                            <IonIcon
+                                v-if="parseInt(item.price) > 0"
+                                :icon="lockClosed"
+                                class="absolute top-1 right-1 text-sm"
+                            />
+                            {{ item.episodeNum }}
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </ion-content>
+    </ion-modal>
+
+    <ion-modal
+        ref="modal"
+        class="pay-modal"
+        :is-open="showPayModal"
+        :initial-breakpoint="1"
+        :breakpoints="[0, 1]"
+        @ionModalDidDismiss="showPayModal = false"
+    >
+        <ion-content>
+            <div class="divide-y divide-neutral-600 text-white">
+                <div class="mb-6">
+                    <div
+                        class="mt-6 text-center text-sm text-prim text-opacity-80"
+                    >
+                        You haven't unlocked this episode yet
+                    </div>
+                    <div class="text-center text-xl mt-2 font-bold">
+                        Join the Membership
+                    </div>
+                    <div class="text-center text-sm mt-1 px-4">
+                        unlock and watch all our videos without any limit
+                    </div>
+                </div>
+                <div class="flex space-x-4 pt-6 px-4">
+                    <div
+                        v-for="(item, n) in plans"
+                        :key="n"
+                        class="h-44 bg-neutral-700 flex-1 rounded-lg flex flex-col items-center border-neutral-500 border-2 overflow-hidden relative"
+                        :class="{ '!border-prim': selectedPlan === n }"
+                        @click="selectedPlan = n"
+                    >
+                        <div
+                            class="absolute top-0 left-0 bg-prim rounded-br-lg text-xs px-2 py-1"
+                            v-if="n === 0"
+                        >
+                            Exclusive
+                        </div>
+                        <div class="mt-8 text-xl font-bold flex-1">
+                            {{ item.title }}
+                        </div>
+                        <div class="flex-1">
+                            <div
+                                class="bg-prim bg-opacity-30 rounded-full px-3 py-1 text-opacity-50 text-sm"
+                            >
+                                {{ item.desc }}
+                            </div>
+                        </div>
+                        <div
+                            class="h-8 bg-neutral-500 self-stretch text-base font-bold flex items-center justify-center"
+                            :class="{ 'bg-prim': selectedPlan === n }"
+                        >
+                            <ion-label color=""> ${{ item.price }}</ion-label>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <IonButton
+                color="tertiary"
+                expand="block"
+                class="mx-4 mt-8 font-bold"
+            >
+                ${{ plans[selectedPlan].price }} Pay Now
+            </IonButton>
+        </ion-content>
+    </ion-modal>
 </template>
 <script setup lang="ts">
-import { Ref, computed, onMounted, reactive, ref, watch } from "vue";
+import { Ref, computed, nextTick, onMounted, reactive, ref, watch } from "vue";
+import {
+    IonIcon,
+    IonSpinner,
+    useIonRouter,
+    IonModal,
+    IonContent,
+    IonLabel,
+    IonButton,
+} from "@ionic/vue";
 import { useDrag } from "@vueuse/gesture";
-import { useElementBounding } from "@vueuse/core";
+import { useElementBounding, useMediaControls } from "@vueuse/core";
+import {
+    chevronForward,
+    chevronBack,
+    arrowRedo,
+    bookmark,
+    ellipsisHorizontal,
+    playCircle,
+    pauseCircle,
+    filter,
+    lockClosed,
+} from "ionicons/icons";
+import http from "@/plugins/http";
 
 const props = defineProps({
     active: Boolean,
+    series: Object,
+    episode: Object,
+    episodes: Array<any>,
 });
-
+const emit = defineEmits(["chooseEpisode"]);
+const router = useIonRouter();
 const video: Ref<HTMLVideoElement | null> = ref(null);
-const progress = ref(20);
+const { playing, currentTime, duration, volume } = useMediaControls(video);
+const userPaused = ref(false);
+const showPauseBtn = ref(false);
+const progress = ref(0);
 const loading = ref(false);
 const dragTarget = ref();
-const isDragging = ref(false);
 const progressBar: Ref<HTMLElement | null> = ref(null);
-
 const draggingData = reactive({
     barWidth: 0,
     videoDuration: 0,
@@ -109,6 +283,29 @@ const draggingData = reactive({
     toProgress: 0,
     dragging: false,
 });
+const playUrl = ref("");
+const needPay = ref(false);
+const showEpisodesModal = ref(false);
+const showPayModal = ref(false);
+const plans = [
+    {
+        title: "7 Days",
+        desc: "Unlimited",
+        price: 0.99,
+    },
+    {
+        title: "1 Month",
+        desc: "Unlimited",
+        price: 19.99,
+    },
+    {
+        title: "3 Months",
+        desc: "Unlimited",
+        price: 49.99,
+    },
+];
+const selectedPlan = ref(0);
+
 function formatDuration(duration: number) {
     const minutes = Math.floor(duration / 60)
         .toString()
@@ -124,7 +321,6 @@ const toDuration = computed(() => {
     );
 });
 const dragHandler = ({ movement: [x, y], dragging }: any) => {
-    console.log(x, dragging);
     if (dragging && !draggingData.dragging) {
         draggingData.barWidth = useElementBounding(progressBar).width.value;
         draggingData.videoDuration = 100;
@@ -144,35 +340,88 @@ const dragHandler = ({ movement: [x, y], dragging }: any) => {
         draggingData.dragging = false;
     }
 };
-useDrag(dragHandler, {
-    domTarget: dragTarget,
-});
 
 function onTimeUpdate() {
-    progress.value = (video.value!.currentTime! / video.value!.duration!) * 100;
+    if (video.value) {
+        progress.value =
+            (video.value!.currentTime! / video.value!.duration!) * 100;
+    }
 }
 
-onMounted(() => {
-    if (props.active) {
-        video.value?.play();
+function getEpisode() {
+    if (!props.episode) return;
+    loading.value = true;
+    try {
+        http.get(`/episodes/${props.episode?.id}`).then((res) => {
+            if (res.playUrl) {
+                playUrl.value = res.playUrl;
+            } else {
+                needPay.value = true;
+            }
+            if (props.active) {
+                nextTick(() => {
+                    onPlay();
+                });
+            }
+        });
+    } catch (error) {
+        console.error(error);
     }
+    loading.value = false;
+}
+
+onMounted(() => {
+    getEpisode();
+    useDrag(dragHandler, {
+        domTarget: dragTarget,
+    });
 });
+watch(
+    () => props.episode,
+    () => {
+        getEpisode();
+    }
+);
 watch(
     () => props.active,
     (active) => {
         if (active) {
-            video.value?.play();
+            onPlay();
         } else {
-            video.value?.pause();
+            playing.value = false;
         }
     }
 );
-function showEpisodes() {
-    console.log("showEpisodes");
+
+const showPlayBtn = computed(() => {
+    if (!props.series || loading.value) return false;
+    if (!playUrl.value) return true;
+    if (userPaused.value) return true;
+    if (!playing.value) return true;
+    return false;
+});
+
+function onPlay() {
+    console.log("onPlay", playUrl.value);
+    if (playUrl.value) {
+        playing.value = true;
+    } else if (needPay.value) {
+        showPayModal.value = true;
+    }
+}
+
+function chooseEpisode(n: number) {
+    showEpisodesModal.value = false;
+    setTimeout(() => {
+        emit("chooseEpisode", n);
+    }, 100);
 }
 </script>
 <style lang="less" scoped>
 .mask-info {
+    .tool-bar {
+        padding-top: var(--ion-safe-area-top);
+    }
     .title-info {
         margin-bottom: 16px;
         margin-bottom: env(safe-area-inset-bottom, 16px);
@@ -211,4 +460,14 @@ function showEpisodes() {
         }
     }
 }
+ion-modal {
+    --height: 50vh;
+}
+.episode-btn.active {
+    background: var(--ion-color-tertiary);
+    color: white;
+}
+.pay-modal {
+    --height: 460px;
+}
 </style>

+ 1 - 1
src/main.ts

@@ -39,5 +39,5 @@ router.isReady().then(() => {
 });
 
 if (process.env.NODE_ENV === "development") {
-    eruda.init();
+    // eruda.init();
 }

+ 18 - 12
src/views/HomeTab.vue

@@ -86,7 +86,7 @@ import { Swiper as VueSwiper, SwiperSlide } from "swiper/vue";
 import { Swiper } from "swiper";
 import { Navigation, Pagination, Scrollbar, A11y } from "swiper/modules";
 import http from "@/plugins/http";
-import { Ref, nextTick, ref } from "vue";
+import { Ref, nextTick, onMounted, ref } from "vue";
 import SeriesItem from "@/components/SeriesItem.vue";
 
 const swiper = ref<Swiper | null>(null);
@@ -94,18 +94,24 @@ const onSwiper = (sw: Swiper) => {
     window.swiper = swiper.value = sw;
 };
 const banners: Ref<any[]> = ref([]);
-http.get("/banners").then((res) => {
-    banners.value = res.data;
-    nextTick(() => {
-        swiper.value?.slideTo(0);
-    });
-});
+
 const categories: Ref<any[]> = ref([{}, {}]);
-http.get("/categories", { order: "sort" }).then((res) => {
-    categories.value = res.data;
-    categories.value.forEach((item) => {
-        http.get("/series", { categories: item.id }).then((res) => {
-            item.series = res.data;
+
+onMounted(() => {
+    http.get("/banners").then((res) => {
+        banners.value = res.data;
+        nextTick(() => {
+            swiper.value?.update();
+            swiper.value?.slideReset();
+            swiper.value?.slideTo(0);
+        });
+    });
+    http.get("/categories", { order: "sort" }).then((res) => {
+        categories.value = res.data;
+        categories.value.forEach((item) => {
+            http.get("/series", { categories: item.id }).then((res) => {
+                item.series = res.data;
+            });
         });
     });
 });

+ 64 - 17
src/views/SeriesView.vue

@@ -2,25 +2,40 @@
     <ion-page>
         <ion-content :fullscreen="true" ref="content">
             <div ref="el" class="w-full h-full">
+                <PlayView v-if="!episodes.length" />
                 <VueSwiper
+                    v-else
+                    class="w-full h-full"
+                    virtual
+                    direction="vertical"
                     :initialSlide="0"
                     :slides-per-view="1"
                     :space-between="0"
-                    :loop="true"
                     :pagination="true"
                     :autoplay="false"
-                    direction="vertical"
                     @swiper="onSwiper"
-                    :modules="[Navigation, Pagination, Scrollbar, A11y]"
-                    class="w-full h-full"
+                    :modules="[
+                        Navigation,
+                        Pagination,
+                        Scrollbar,
+                        A11y,
+                        Virtual,
+                    ]"
+                    @slide-change="onSlideChange"
                 >
                     <swiper-slide
-                        v-slot="{ isActive }"
-                        v-for="n in 10"
-                        :key="n"
+                        v-for="(item, n) in episodes"
+                        :key="item.id"
                         class="w-full h-full"
+                        :virtualIndex="item.id"
                     >
-                        <PlayView :active="isActive" />
+                        <PlayView
+                            :active="n === activeSlide"
+                            :series="series"
+                            :episode="item"
+                            :episodes="episodes"
+                            @choose-episode="onChooseEpisode"
+                        />
                     </swiper-slide>
                 </VueSwiper>
             </div>
@@ -28,19 +43,21 @@
     </ion-page>
 </template>
 <script setup lang="ts">
-import {
-    IonPage,
-    IonHeader,
-    IonToolbar,
-    IonTitle,
-    IonContent,
-} from "@ionic/vue";
+import { IonPage, IonContent, useIonRouter } from "@ionic/vue";
 import { Swiper as VueSwiper, SwiperSlide } from "swiper/vue";
 import { Swiper } from "swiper";
-import { Navigation, Pagination, Scrollbar, A11y } from "swiper/modules";
+import {
+    Navigation,
+    Pagination,
+    Scrollbar,
+    A11y,
+    Virtual,
+} from "swiper/modules";
 import PlayView from "@/components/PlayView.vue";
-import { ref } from "vue";
+import { nextTick, onMounted, reactive, ref } from "vue";
 import { useElementBounding } from "@vueuse/core";
+import { useRoute } from "vue-router";
+import http from "@/plugins/http";
 
 const el = ref<HTMLElement | null>(null);
 const { x, y, top, right, bottom, left, width, height } =
@@ -50,5 +67,35 @@ const swiper = ref<Swiper | null>(null);
 const onSwiper = (sw: Swiper) => {
     window.swiper = swiper.value = sw;
 };
+const activeSlide = ref(0);
+
+const route = useRoute();
+const router = useIonRouter();
+
+const series = ref<any>(null);
+const episodes = ref<any[]>([]);
+
+onMounted(() => {
+    http.get(`/series/${route.params.id}`).then((res) => {
+        series.value = res;
+        http.get(`/episodes`, {
+            seriesId: route.params.id,
+            order: "episodeNum",
+            pageSize: 1000,
+        }).then((res) => {
+            episodes.value = res.data;
+            nextTick(() => {
+                swiper.value?.update();
+            });
+        });
+    });
+});
+function onSlideChange(e: any) {
+    activeSlide.value = e.activeIndex;
+}
+
+function onChooseEpisode(n: number) {
+    swiper.value?.slideTo(n, 0);
+}
 </script>
 <style lang="less" scoped></style>

+ 2 - 2
src/views/TabsPage.vue

@@ -13,7 +13,7 @@
                     <ion-label>Home</ion-label>
                 </ion-tab-button>
 
-                <ion-tab-button tab="forYou" href="/tabs/forYou">
+                <!-- <ion-tab-button tab="forYou" href="/tabs/forYou">
                     <ion-icon
                         aria-hidden="true"
                         :icon="IconTabPlayPre"
@@ -21,7 +21,7 @@
                     />
                     <ion-icon aria-hidden="true" :icon="IconTabPlay" v-else />
                     <ion-label>For You</ion-label>
-                </ion-tab-button>
+                </ion-tab-button> -->
 
                 <ion-tab-button tab="myList" href="/tabs/myList">
                     <ion-icon

+ 5 - 1
tailwind.config.js

@@ -2,7 +2,11 @@
 export default {
     content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
     theme: {
-        extend: {},
+        extend: {
+            colors: {
+                prim: "#FF136A",
+            },
+        },
     },
     plugins: [],
     darkMode: "class",

+ 1 - 0
vite.config.ts

@@ -13,5 +13,6 @@ export default defineConfig({
     },
     server: {
         host: "0.0.0.0",
+        port: 4173
     },
 });

+ 10 - 0
yarn.lock

@@ -963,6 +963,11 @@
     "@babel/helper-validator-identifier" "^7.22.20"
     to-fast-properties "^2.0.0"
 
+"@capacitor/android@^5.7.2":
+  version "5.7.2"
+  resolved "https://registry.npmmirror.com/@capacitor/android/-/android-5.7.2.tgz#a6eed6398e62aae3b87df3785bf3821309d8e327"
+  integrity sha512-T4U+15R/1PyokW0Le92j7AV19kuO25his2ymF2xf2I04fZUDj8RjmXA+za7i3K8vhxtKkTdY2dPAywrfNAM09Q==
+
 "@capacitor/app@5.0.7":
   version "5.0.7"
   resolved "https://registry.npmmirror.com/@capacitor/app/-/app-5.0.7.tgz#f71e81320bcd504c400c1f503ba43a88ccac96e8"
@@ -1003,6 +1008,11 @@
   resolved "https://registry.npmmirror.com/@capacitor/haptics/-/haptics-5.0.7.tgz#7ae294b7180f08c27f9c8af12f3c5f2a8057d5ba"
   integrity sha512-/j+7Qa4BxQA5aOU43cwXuiudfSXfoHFsAVfcehH5DkSjxLykZKWHEuE4uFJXqdkSIbAHjS37D0Sde6ENP6G/MA==
 
+"@capacitor/ios@^5.7.2":
+  version "5.7.2"
+  resolved "https://registry.npmmirror.com/@capacitor/ios/-/ios-5.7.2.tgz#1d68001bca21c12520fc23bcad45af5730193a78"
+  integrity sha512-msh+Kqjv/MyVCrSH0zVtwxptXnsgky4FENUq+Xdaa1pqEglmpHlUKod1Jf7qhfAhTLhHPyokOZMvaIyTtoSwCA==
+
 "@capacitor/keyboard@5.0.8":
   version "5.0.8"
   resolved "https://registry.npmmirror.com/@capacitor/keyboard/-/keyboard-5.0.8.tgz#b8cc21bd6e06273ec61974be9d7f4d8c421a63a4"