├── .fvm
└── fvm_config.json
├── .gitignore
├── .metadata
├── README.md
├── analysis_options.yaml
├── android
├── .gitignore
├── app
│ ├── build.gradle
│ └── src
│ │ ├── debug
│ │ └── AndroidManifest.xml
│ │ ├── main
│ │ ├── AndroidManifest.xml
│ │ ├── feature-graphic.png
│ │ ├── ic_launcher-web.png
│ │ ├── kotlin
│ │ │ └── jbscript
│ │ │ │ └── remote_control_for_vlc
│ │ │ │ └── MainActivity.kt
│ │ └── res
│ │ │ ├── drawable-hdpi
│ │ │ └── ic_launcher_foreground.png
│ │ │ ├── drawable-mdpi
│ │ │ └── ic_launcher_foreground.png
│ │ │ ├── drawable-v21
│ │ │ └── launch_background.xml
│ │ │ ├── drawable-xhdpi
│ │ │ └── ic_launcher_foreground.png
│ │ │ ├── drawable-xxhdpi
│ │ │ └── ic_launcher_foreground.png
│ │ │ ├── drawable-xxxhdpi
│ │ │ └── ic_launcher_foreground.png
│ │ │ ├── drawable
│ │ │ ├── ic_launcher_background.xml
│ │ │ ├── launch_background.xml
│ │ │ ├── launch_icon.png
│ │ │ └── normal_background.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ │ ├── mipmap-hdpi
│ │ │ ├── ic_launcher.png
│ │ │ ├── ic_launcher_foreground.png
│ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-mdpi
│ │ │ ├── ic_launcher.png
│ │ │ ├── ic_launcher_foreground.png
│ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xhdpi
│ │ │ ├── ic_launcher.png
│ │ │ ├── ic_launcher_foreground.png
│ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ ├── ic_launcher_foreground.png
│ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ ├── ic_launcher_foreground.png
│ │ │ └── ic_launcher_round.png
│ │ │ ├── values-night
│ │ │ └── styles.xml
│ │ │ └── values
│ │ │ ├── ic_launcher_background.xml
│ │ │ └── styles.xml
│ │ └── profile
│ │ └── AndroidManifest.xml
├── build.gradle
├── gradle.properties
├── gradle
│ └── wrapper
│ │ └── gradle-wrapper.properties
└── settings.gradle
├── assets
├── cone.png
├── icon-512.png
├── linux-ifconfig.png
├── linux-lua.png
├── linux-main-interface.png
├── linux-menu.png
├── linux-password.png
├── linux-show-settings.png
├── linux-terminal.png
├── linux-web.png
├── mac-http-interface.png
├── mac-ip.png
├── mac-menu.png
├── mac-network.png
├── signal-0.png
├── signal-1.png
├── signal-2.png
├── signal-3.png
├── windows-cmd.png
├── windows-ipconfig.png
├── windows-lua.png
├── windows-main-interface.png
├── windows-menu.png
├── windows-password.png
├── windows-run.png
├── windows-show-settings.png
└── windows-web.png
├── icons
├── google-play-icon.png
├── icon.png
└── icon.svg
├── lib
├── equalizer_screen.dart
├── file_browser.dart
├── host_ip_guide.dart
├── main.dart
├── models.dart
├── open_media.dart
├── remote_control.dart
├── settings_screen.dart
├── utils.dart
├── vlc_configuration_guide.dart
└── widgets.dart
├── pubspec.lock
├── pubspec.yaml
└── screenshots
├── equalizer.png
├── file-browser.png
├── host-ip-guide-os.png
├── open-media.png
├── playback-speed.png
├── playing-menu-vlc.png
├── playing-vlc.png
├── settings.png
├── setup-guide-os.png
├── setup-guide-steps.png
├── setup.png
└── vlc-connected.png
/.fvm/fvm_config.json:
--------------------------------------------------------------------------------
1 | {
2 | "flutterSdkVersion": "3.0.0",
3 | "flavors": {}
4 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .fvm/flutter_sdk
2 | demo-playlist.xspf
3 |
4 | # Miscellaneous
5 | *.class
6 | *.log
7 | *.pyc
8 | *.swp
9 | .DS_Store
10 | .atom/
11 | .buildlog/
12 | .history
13 | .svn/
14 | migrate_working_dir/
15 |
16 | # IntelliJ related
17 | *.iml
18 | *.ipr
19 | *.iws
20 | .idea/
21 |
22 | # The .vscode folder contains launch configuration and tasks you configure in
23 | # VS Code which you may wish to be included in version control, so this line
24 | # is commented out by default.
25 | #.vscode/
26 |
27 | # Flutter/Dart/Pub related
28 | **/doc/api/
29 | **/ios/Flutter/.last_build_id
30 | .dart_tool/
31 | .flutter-plugins
32 | .flutter-plugins-dependencies
33 | .packages
34 | .pub-cache/
35 | .pub/
36 | /build/
37 |
38 | # Web related
39 | lib/generated_plugin_registrant.dart
40 |
41 | # Symbolication related
42 | app.*.symbols
43 |
44 | # Obfuscation related
45 | app.*.map.json
46 |
47 | # Android Studio will place build artifacts here
48 | /android/app/debug
49 | /android/app/profile
50 | /android/app/release
51 |
--------------------------------------------------------------------------------
/.metadata:
--------------------------------------------------------------------------------
1 | # This file tracks properties of this Flutter project.
2 | # Used by Flutter tool to assess capabilities and perform upgrades etc.
3 | #
4 | # This file should be version controlled and should not be manually edited.
5 |
6 | version:
7 | revision:
8 | channel: unknown
9 |
10 | project_type: app
11 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Remote Control for VLC
2 |
3 | A [VLC](https://www.videolan.org/vlc/) remote control written with [Flutter](https://flutter.io/).
4 |
5 | ## Initial Setup Guides
6 |
7 | The first time you start Remote Control for VLC, it will provide a guide to setting up VLC's web interface for remote control and will give you the option to try to discover the IP of the host VLC is running on automatically.
8 |
9 | The app's guides for initial VLC configuration and finding your host IP if it can't be found automatically are also available here if you need to reference them on a computer:
10 |
11 | ### [Remote Control for VLC Configuration Guides](https://insin.github.io/remote-control-for-vlc/)
12 |
13 | ## Demo
14 |
15 | [](https://www.youtube.com/watch?v=8eXJX4GVGhA)
16 |
17 | ## Screenshots
18 |
19 | ## Initial setup screens
20 |
21 | | Setup screen | Choose OS for setup guide | Setup guide | Connected |
22 | | ------------- | ------------------------- | ----------- | --------- |
23 | | [](screenshots/setup.png) | [](screenshots/setup-guide-os.png) | [](screenshots/setup-guide-steps.png) | [](screenshots/vlc-connected.png) |
24 |
25 | ### Browsing for and playing media
26 |
27 | | Open media | File browser | Playlist | Playlist Menu |
28 | | ----------- | ------------ | -------- | ------------- |
29 | | [](screenshots/open-media.png) | [](screenshots/file-browser.png) | [](screenshots/playing-vlc.png) | [](screenshots/playing-menu-vlc.png) |
30 |
31 | ### Settings
32 |
33 | | Settings screen | Host IP guide | Equalizer | Playback Speed |
34 | | ---------------- | ------------- | --------- | -------------- |
35 | | [](screenshots/settings.png) | [](screenshots/host-ip-guide-os.png) | [](screenshots/equalizer.png) | [](screenshots/playback-speed.png) |
36 |
--------------------------------------------------------------------------------
/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | # This file configures the analyzer, which statically analyzes Dart code to
2 | # check for errors, warnings, and lints.
3 | #
4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled
5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
6 | # invoked from the command line by running `flutter analyze`.
7 |
8 | # The following line activates a set of recommended lints for Flutter apps,
9 | # packages, and plugins designed to encourage good coding practices.
10 | include: package:flutter_lints/flutter.yaml
11 |
12 | linter:
13 | # The lint rules applied to this project can be customized in the
14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml`
15 | # included above or to enable additional rules. A list of all available lints
16 | # and their documentation is published at
17 | # https://dart-lang.github.io/linter/lints/index.html.
18 | #
19 | # Instead of disabling a lint rule for the entire project in the
20 | # section below, it can also be suppressed for a single line of code
21 | # or a specific dart file by using the `// ignore: name_of_lint` and
22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file
23 | # producing the lint.
24 | rules:
25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule
26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
27 |
28 | # Additional information about this file can be found at
29 | # https://dart.dev/guides/language/analysis-options
30 |
--------------------------------------------------------------------------------
/android/.gitignore:
--------------------------------------------------------------------------------
1 | gradle-wrapper.jar
2 | /.gradle
3 | /captures/
4 | /gradlew
5 | /gradlew.bat
6 | /local.properties
7 | GeneratedPluginRegistrant.java
8 |
9 | # Remember to never publicly share your keystore.
10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
11 | key.properties
12 | **/*.keystore
13 | **/*.jks
14 |
--------------------------------------------------------------------------------
/android/app/build.gradle:
--------------------------------------------------------------------------------
1 | def localProperties = new Properties()
2 | def localPropertiesFile = rootProject.file('local.properties')
3 | if (localPropertiesFile.exists()) {
4 | localPropertiesFile.withReader('UTF-8') { reader ->
5 | localProperties.load(reader)
6 | }
7 | }
8 |
9 | def flutterRoot = localProperties.getProperty('flutter.sdk')
10 | if (flutterRoot == null) {
11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
12 | }
13 |
14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
15 | if (flutterVersionCode == null) {
16 | flutterVersionCode = '1'
17 | }
18 |
19 | def flutterVersionName = localProperties.getProperty('flutter.versionName')
20 | if (flutterVersionName == null) {
21 | flutterVersionName = '1.0'
22 | }
23 |
24 | apply plugin: 'com.android.application'
25 | apply plugin: 'kotlin-android'
26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
27 |
28 | android {
29 | compileSdkVersion flutter.compileSdkVersion
30 | ndkVersion flutter.ndkVersion
31 |
32 | compileOptions {
33 | sourceCompatibility JavaVersion.VERSION_1_8
34 | targetCompatibility JavaVersion.VERSION_1_8
35 | }
36 |
37 | kotlinOptions {
38 | jvmTarget = '1.8'
39 | }
40 |
41 | sourceSets {
42 | main.java.srcDirs += 'src/main/kotlin'
43 | }
44 |
45 | defaultConfig {
46 | applicationId "jbscript.vlcremote"
47 | minSdkVersion flutter.minSdkVersion
48 | targetSdkVersion flutter.targetSdkVersion
49 | versionCode flutterVersionCode.toInteger()
50 | versionName flutterVersionName
51 | }
52 |
53 | signingConfigs {
54 | release {
55 | def keystorePropertiesFile = rootProject.file('key.properties')
56 |
57 | if (!keystorePropertiesFile.exists()) return;
58 |
59 | def keystoreProperties = new Properties()
60 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
61 |
62 | keyAlias keystoreProperties['keyAlias']
63 | keyPassword keystoreProperties['keyPassword']
64 | storeFile file(keystoreProperties['storeFile'])
65 | storePassword keystoreProperties['storePassword']
66 | }
67 | }
68 |
69 | buildTypes {
70 | release {
71 | signingConfig signingConfigs.release
72 | }
73 | }
74 | }
75 |
76 | flutter {
77 | source '../..'
78 | }
79 |
80 | dependencies {
81 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
82 | }
83 |
--------------------------------------------------------------------------------
/android/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
9 |
17 |
21 |
25 |
26 |
27 |
28 |
29 |
30 |
32 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/android/app/src/main/feature-graphic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/android/app/src/main/feature-graphic.png
--------------------------------------------------------------------------------
/android/app/src/main/ic_launcher-web.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/android/app/src/main/ic_launcher-web.png
--------------------------------------------------------------------------------
/android/app/src/main/kotlin/jbscript/remote_control_for_vlc/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package jbscript.remote_control_for_vlc
2 |
3 | import io.flutter.embedding.android.FlutterActivity
4 |
5 | class MainActivity: FlutterActivity() {
6 | }
7 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-v21/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | -
5 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
10 |
12 |
14 |
16 |
18 |
20 |
22 |
24 |
26 |
28 |
30 |
32 |
34 |
36 |
38 |
40 |
42 |
44 |
46 |
48 |
50 |
52 |
54 |
56 |
58 |
60 |
62 |
64 |
66 |
68 |
70 |
72 |
74 |
75 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | -
5 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/launch_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/android/app/src/main/res/drawable/launch_icon.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/normal_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/app/src/main/res/values-night/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFFFFF
4 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/android/app/src/profile/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | ext.kotlin_version = '1.6.10'
3 | repositories {
4 | google()
5 | mavenCentral()
6 | }
7 |
8 | dependencies {
9 | classpath 'com.android.tools.build:gradle:7.1.2'
10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
11 | }
12 | }
13 |
14 | allprojects {
15 | repositories {
16 | google()
17 | mavenCentral()
18 | }
19 | }
20 |
21 | rootProject.buildDir = '../build'
22 | subprojects {
23 | project.buildDir = "${rootProject.buildDir}/${project.name}"
24 | }
25 | subprojects {
26 | project.evaluationDependsOn(':app')
27 | }
28 |
29 | task clean(type: Delete) {
30 | delete rootProject.buildDir
31 | }
32 |
--------------------------------------------------------------------------------
/android/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx1536M
2 | android.useAndroidX=true
3 | android.enableJetifier=true
4 |
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri Jun 23 08:50:38 CEST 2017
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
7 |
--------------------------------------------------------------------------------
/android/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
4 | def properties = new Properties()
5 |
6 | assert localPropertiesFile.exists()
7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
8 |
9 | def flutterSdkPath = properties.getProperty("flutter.sdk")
10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
12 |
--------------------------------------------------------------------------------
/assets/cone.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/assets/cone.png
--------------------------------------------------------------------------------
/assets/icon-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/assets/icon-512.png
--------------------------------------------------------------------------------
/assets/linux-ifconfig.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/assets/linux-ifconfig.png
--------------------------------------------------------------------------------
/assets/linux-lua.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/assets/linux-lua.png
--------------------------------------------------------------------------------
/assets/linux-main-interface.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/assets/linux-main-interface.png
--------------------------------------------------------------------------------
/assets/linux-menu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/assets/linux-menu.png
--------------------------------------------------------------------------------
/assets/linux-password.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/assets/linux-password.png
--------------------------------------------------------------------------------
/assets/linux-show-settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/assets/linux-show-settings.png
--------------------------------------------------------------------------------
/assets/linux-terminal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/assets/linux-terminal.png
--------------------------------------------------------------------------------
/assets/linux-web.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/assets/linux-web.png
--------------------------------------------------------------------------------
/assets/mac-http-interface.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/assets/mac-http-interface.png
--------------------------------------------------------------------------------
/assets/mac-ip.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/assets/mac-ip.png
--------------------------------------------------------------------------------
/assets/mac-menu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/assets/mac-menu.png
--------------------------------------------------------------------------------
/assets/mac-network.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/assets/mac-network.png
--------------------------------------------------------------------------------
/assets/signal-0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/assets/signal-0.png
--------------------------------------------------------------------------------
/assets/signal-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/assets/signal-1.png
--------------------------------------------------------------------------------
/assets/signal-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/assets/signal-2.png
--------------------------------------------------------------------------------
/assets/signal-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/assets/signal-3.png
--------------------------------------------------------------------------------
/assets/windows-cmd.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/assets/windows-cmd.png
--------------------------------------------------------------------------------
/assets/windows-ipconfig.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/assets/windows-ipconfig.png
--------------------------------------------------------------------------------
/assets/windows-lua.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/assets/windows-lua.png
--------------------------------------------------------------------------------
/assets/windows-main-interface.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/assets/windows-main-interface.png
--------------------------------------------------------------------------------
/assets/windows-menu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/assets/windows-menu.png
--------------------------------------------------------------------------------
/assets/windows-password.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/assets/windows-password.png
--------------------------------------------------------------------------------
/assets/windows-run.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/assets/windows-run.png
--------------------------------------------------------------------------------
/assets/windows-show-settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/assets/windows-show-settings.png
--------------------------------------------------------------------------------
/assets/windows-web.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/assets/windows-web.png
--------------------------------------------------------------------------------
/icons/google-play-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/icons/google-play-icon.png
--------------------------------------------------------------------------------
/icons/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/icons/icon.png
--------------------------------------------------------------------------------
/icons/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
633 |
--------------------------------------------------------------------------------
/lib/equalizer_screen.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'dart:math' as math;
3 |
4 | import 'package:flutter/material.dart';
5 | import 'package:just_throttle_it/just_throttle_it.dart';
6 |
7 | import 'models.dart';
8 | import 'widgets.dart';
9 |
10 | String _decibelsToString(double db) {
11 | var string = db.toStringAsFixed(1);
12 | if (string == '-0.0') {
13 | string = '0.0';
14 | }
15 | return string;
16 | }
17 |
18 | var _frequencies = [
19 | '60Hz',
20 | '170Hz',
21 | '310Hz',
22 | '600Hz',
23 | '1KHz',
24 | '3KHz',
25 | '6KHz',
26 | '12KHz',
27 | '14KHz',
28 | '16KHz'
29 | ];
30 |
31 | class EqualizerScreen extends StatefulWidget {
32 | final Equalizer equalizer;
33 | final Stream equalizerStream;
34 | final Future Function(bool enabled) onToggleEnabled;
35 | final Future Function(int presetId) onPresetChange;
36 | final Future Function(String db) onPreampChange;
37 | final Future Function(int bandId, String db) onBandChange;
38 |
39 | const EqualizerScreen({
40 | Key? key,
41 | required this.equalizer,
42 | required this.equalizerStream,
43 | required this.onToggleEnabled,
44 | required this.onPresetChange,
45 | required this.onPreampChange,
46 | required this.onBandChange,
47 | }) : super(key: key);
48 |
49 | @override
50 | State createState() => _EqualizerScreenState();
51 | }
52 |
53 | class _EqualizerScreenState extends State {
54 | /// The most recent equalizer status from VLC.
55 | late Equalizer _equalizer = widget.equalizer;
56 |
57 | /// Used to listen for equalizer status updates from VLC.
58 | late final StreamSubscription _equalizerSubscription =
59 | widget.equalizerStream.listen(_onEqualizer);
60 |
61 | /// TODO Extract preset settings from VLC and use their preamp and band values to match a preset name
62 | /// The last-selected preset - selected preset info isn't available from VLC.
63 | Preset? _preset;
64 |
65 | /// The value for the Preamp slider during and after finishing dragging it.
66 | ///
67 | /// This will be removed once the preamp value from a VLC status update
68 | /// matches it.
69 | double? _preamp;
70 |
71 | /// Used to ignore equalizer status updates VLC after a band change finishes
72 | /// while sending requests to update equalizer bands.
73 | bool _ignoreStatusUpdates = false;
74 |
75 | /// When `true`, equalizer bands close to the band being adjusted will be
76 | /// adjusted in the same direction proportional to the amount the band has
77 | /// changed, falling off as proximity to the changing band decreases.
78 | bool _snapBands = true;
79 |
80 | /// The id/index of the band slider currently being dragged.
81 | int? _draggingBand;
82 |
83 | /// The initial equalizer state when a band slider started dragging.- used to
84 | /// calculate values for other bands when [_snapBands] is `true.
85 | ///
86 | /// To prevent band values jumping to their former values after a drag
87 | /// finishes, values for modified bands are stored in this object, which is
88 | /// used for display until equalizer band values from VLC status updates match
89 | /// its values.
90 | Equalizer? _draggingEqualizer;
91 |
92 | /// The value [_draggingBand] had when its drag started
93 | ///
94 | /// Used with [_dragValue] to calculate the delta when snapping bands.
95 | double? _dragStartValue;
96 |
97 | /// The current value of the band being dragged.
98 | double? _dragValue;
99 |
100 | @override
101 | void dispose() {
102 | _equalizerSubscription.cancel();
103 | super.dispose();
104 | }
105 |
106 | _onEqualizer(Equalizer? equalizer) {
107 | if (equalizer == null) {
108 | Navigator.pop(context);
109 | return;
110 | }
111 | if (_ignoreStatusUpdates) {
112 | return;
113 | }
114 | setState(() {
115 | // Get rid of the equalizer containing values from a finished EQ change
116 | // once the equalizer from VLC status updates matches it.
117 | if (_draggingBand == null &&
118 | _draggingEqualizer != null &&
119 | _draggingEqualizer!.bands.every((band) =>
120 | _decibelsToString(band.value) ==
121 | _decibelsToString(equalizer.bands[band.id].value))) {
122 | _draggingEqualizer = null;
123 | }
124 | _equalizer = equalizer;
125 | });
126 | }
127 |
128 | _toggleEnabled(enabled) async {
129 | Equalizer? equalizer = await widget.onToggleEnabled(enabled);
130 | if (equalizer == null) {
131 | if (mounted) {
132 | Navigator.pop(context);
133 | }
134 | return;
135 | }
136 | setState(() {
137 | _equalizer = equalizer;
138 | });
139 | }
140 |
141 | _choosePreset() async {
142 | var preset = await showDialog(
143 | context: context,
144 | builder: (context) => SimpleDialog(
145 | title: const Text('Preset'),
146 | children: _equalizer.presets
147 | .map((preset) => SimpleDialogOption(
148 | child: Text(preset.name),
149 | onPressed: () {
150 | Navigator.pop(context, preset);
151 | },
152 | ))
153 | .toList(),
154 | ),
155 | );
156 | if (preset == null) {
157 | return;
158 | }
159 | setState(() {
160 | _preset = preset;
161 | });
162 | var equalizer = await widget.onPresetChange(preset.id);
163 | if (equalizer == null) {
164 | if (mounted) {
165 | Navigator.pop(context);
166 | }
167 | return;
168 | }
169 | setState(() {
170 | _equalizer = equalizer;
171 | });
172 | }
173 |
174 | _onPreampChanged(preamp) async {
175 | var equalizer = await widget.onPreampChange(_decibelsToString(preamp));
176 | if (equalizer == null) {
177 | if (mounted) {
178 | Navigator.pop(context);
179 | }
180 | return;
181 | }
182 | setState(() {
183 | _equalizer = equalizer;
184 | _preamp = null;
185 | });
186 | }
187 |
188 | double _getBandValue(int band) {
189 | // Not dragging, use the current value
190 | // If we finished changing a band, use the new values until they're current
191 | if (_draggingBand == null) {
192 | return (_draggingEqualizer ?? _equalizer).bands[band].value;
193 | }
194 | // The dragging band always uses the drag value
195 | if (band == _draggingBand) {
196 | return _dragValue!;
197 | }
198 | // If we're not snapping, other bands use the current value
199 | if (!_snapBands) {
200 | return _equalizer.bands[band].value;
201 | }
202 | // Otherwise add portions of the size of the change to neighbouring bands
203 | var distance = (band - _draggingBand!).abs();
204 | switch (distance) {
205 | case 1:
206 | return (_draggingEqualizer!.bands[band].value +
207 | ((_dragValue! - _dragStartValue!) / 2))
208 | .clamp(-20.0, 20.0);
209 | case 2:
210 | return (_draggingEqualizer!.bands[band].value +
211 | ((_dragValue! - _dragStartValue!) / 8))
212 | .clamp(-20.0, 20.0);
213 | case 3:
214 | return (_draggingEqualizer!.bands[band].value +
215 | ((_dragValue! - _dragStartValue!) / 40))
216 | .clamp(-20.0, 20.0);
217 | default:
218 | return _equalizer.bands[band].value;
219 | }
220 | }
221 |
222 | _onBandChangeStart(int band, double value) {
223 | setState(() {
224 | _draggingEqualizer = _equalizer;
225 | _draggingBand = band;
226 | _dragStartValue = value;
227 | _dragValue = value;
228 | });
229 | }
230 |
231 | _onBandChanged(double value) {
232 | setState(() {
233 | _dragValue = value;
234 | });
235 | }
236 |
237 | _onBandChangeEnd(double value) async {
238 | List bandChanges = [];
239 | if (!_snapBands) {
240 | _draggingEqualizer!.bands[_draggingBand!].value = value;
241 | bandChanges.add(Band(_draggingBand!, value));
242 | } else {
243 | for (int band = math.max(0, _draggingBand! - 3);
244 | band < math.min(_frequencies.length, _draggingBand! + 4);
245 | band++) {
246 | var value = _getBandValue(band);
247 | // Store new values to display while VLC status catches up
248 | _draggingEqualizer!.bands[band].value = value;
249 | bandChanges.add(Band(band, value));
250 | }
251 | }
252 | setState(() {
253 | _draggingBand = null;
254 | _dragStartValue = null;
255 | _dragValue = null;
256 | });
257 | _ignoreStatusUpdates = true;
258 | await Future.wait(bandChanges.map(
259 | (band) => widget.onBandChange(band.id, _decibelsToString(band.value))));
260 | _ignoreStatusUpdates = false;
261 | }
262 |
263 | @override
264 | Widget build(BuildContext context) {
265 | var theme = Theme.of(context);
266 | return Scaffold(
267 | appBar: AppBar(
268 | title: Text(intl('Equalizer')),
269 | ),
270 | body: ListView(children: [
271 | SwitchListTile(
272 | title: const Text('Enable', textAlign: TextAlign.right),
273 | value: _equalizer.enabled,
274 | onChanged: _toggleEnabled,
275 | ),
276 | if (_equalizer.enabled)
277 | Column(children: [
278 | ListTile(
279 | title: const Text('Preset'),
280 | subtitle: Text(_preset?.name ?? 'Tap to select'),
281 | onTap: _choosePreset,
282 | ),
283 | Padding(
284 | padding: const EdgeInsets.symmetric(horizontal: 16),
285 | child: Row(
286 | children: [
287 | Text('Preamp', style: theme.textTheme.subtitle1),
288 | const SizedBox(width: 16),
289 | Expanded(
290 | child: SliderTheme(
291 | data: SliderTheme.of(context).copyWith(
292 | trackShape: FullWidthTrackShape(),
293 | ),
294 | child: Slider(
295 | max: 20,
296 | min: -20,
297 | value: _preamp ?? _equalizer.preamp,
298 | onChanged: (db) {
299 | setState(() {
300 | _preamp = db;
301 | });
302 | Throttle.milliseconds(333, widget.onPreampChange,
303 | [_decibelsToString(db)]);
304 | },
305 | onChangeEnd: _onPreampChanged,
306 | ),
307 | ),
308 | )
309 | ],
310 | ),
311 | ),
312 | const SizedBox(height: 16),
313 | Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
314 | for (int i = 0; i < _frequencies.length; i++)
315 | _VerticalBandSlider(
316 | label: _frequencies[i],
317 | band: i,
318 | value: _getBandValue(i),
319 | onChangeStart: _onBandChangeStart,
320 | onChanged: _onBandChanged,
321 | onChangeEnd: _onBandChangeEnd,
322 | ),
323 | ]),
324 | SwitchListTile(
325 | title: const Text('Snap bands', textAlign: TextAlign.right),
326 | value: _snapBands,
327 | onChanged: (snapBands) {
328 | setState(() {
329 | _snapBands = snapBands;
330 | });
331 | },
332 | ),
333 | ]),
334 | ]),
335 | );
336 | }
337 | }
338 |
339 | class _VerticalBandSlider extends StatefulWidget {
340 | final String label;
341 | final int band;
342 | final double value;
343 | final Function(int band, double value) onChangeStart;
344 | final Function(double value) onChanged;
345 | final Function(double value) onChangeEnd;
346 |
347 | const _VerticalBandSlider({
348 | required this.label,
349 | required this.band,
350 | required this.value,
351 | required this.onChangeStart,
352 | required this.onChanged,
353 | required this.onChangeEnd,
354 | });
355 |
356 | @override
357 | _VerticalBandSliderState createState() => _VerticalBandSliderState();
358 | }
359 |
360 | class _VerticalBandSliderState extends State<_VerticalBandSlider> {
361 | @override
362 | Widget build(BuildContext context) {
363 | return Column(children: [
364 | const Text('+20dB', style: TextStyle(fontSize: 10)),
365 | const SizedBox(height: 16),
366 | RotatedBox(
367 | quarterTurns: -1,
368 | child: SliderTheme(
369 | data: SliderTheme.of(context).copyWith(
370 | trackShape: FullWidthTrackShape(),
371 | thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 8),
372 | overlayShape: const RoundSliderOverlayShape(overlayRadius: 16),
373 | ),
374 | child: Slider(
375 | max: 20,
376 | min: -20,
377 | value: widget.value,
378 | onChangeStart: (value) {
379 | widget.onChangeStart(widget.band, value);
380 | },
381 | onChanged: (value) {
382 | widget.onChanged(value);
383 | },
384 | onChangeEnd: (value) {
385 | widget.onChangeEnd(value);
386 | },
387 | ),
388 | ),
389 | ),
390 | const SizedBox(height: 16),
391 | const Text('-20dB', style: TextStyle(fontSize: 10)),
392 | const SizedBox(height: 8),
393 | Text(widget.label,
394 | style: const TextStyle(fontSize: 10, fontWeight: FontWeight.bold)),
395 | ]);
396 | }
397 | }
398 |
--------------------------------------------------------------------------------
/lib/file_browser.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:http/http.dart' as http;
5 | import 'package:xml/xml.dart' as xml;
6 |
7 | import 'models.dart';
8 | import 'widgets.dart';
9 |
10 | class FileBrowser extends StatefulWidget {
11 | final BrowseItem dir;
12 | final bool Function(BrowseItem) isFave;
13 | final Function(BrowseItem) onToggleFave;
14 | final Settings settings;
15 |
16 | const FileBrowser({
17 | Key? key,
18 | required this.dir,
19 | required this.isFave,
20 | required this.onToggleFave,
21 | required this.settings,
22 | }) : super(key: key);
23 |
24 | @override
25 | State createState() => _FileBrowserState();
26 | }
27 |
28 | class _FileBrowserState extends State {
29 | bool _loading = false;
30 | String? _errorMessage;
31 | String? _errorDetail;
32 | List _items = [];
33 |
34 | @override
35 | void initState() {
36 | _getListing(widget.dir);
37 | super.initState();
38 | }
39 |
40 | _getListing(BrowseItem dir) async {
41 | setState(() {
42 | _loading = true;
43 | });
44 |
45 | http.Response response;
46 |
47 | assert(() {
48 | //print('/requests/browse.xml?uri=${dir.uri}');
49 | return true;
50 | }());
51 |
52 | try {
53 | response = await http.get(
54 | Uri.http(widget.settings.connection.authority, '/requests/browse.xml', {
55 | 'uri': dir.uri,
56 | }),
57 | headers: {
58 | 'Authorization':
59 | 'Basic ${base64Encode(utf8.encode(':${widget.settings.connection.password}'))}',
60 | },
61 | ).timeout(const Duration(seconds: 2));
62 | } catch (e) {
63 | setState(() {
64 | _errorMessage = 'Error connecting to VLC';
65 | _errorDetail = e.runtimeType.toString();
66 | _loading = false;
67 | });
68 | return;
69 | }
70 |
71 | List dirs = [];
72 | List files = [];
73 |
74 | if (response.statusCode == 200) {
75 | var document = xml.XmlDocument.parse(utf8.decode(response.bodyBytes));
76 | document.findAllElements('element').forEach((el) {
77 | var item = BrowseItem(
78 | el.getAttribute('type') ?? '',
79 | el.getAttribute('name') ?? '',
80 | el.getAttribute('path') ?? '',
81 | el.getAttribute('uri') ?? '',
82 | );
83 |
84 | if (item.name == '..') {
85 | return;
86 | }
87 |
88 | if (item.isDir) {
89 | dirs.add(item);
90 | } else if (item.isSupportedMedia || item.isPlaylist) {
91 | files.add(item);
92 | }
93 | });
94 | }
95 |
96 | dirs.sort((a, b) {
97 | return a.name.toLowerCase().compareTo(b.name.toLowerCase());
98 | });
99 | files.sort((a, b) {
100 | return a.name.toLowerCase().compareTo(b.name.toLowerCase());
101 | });
102 |
103 | setState(() {
104 | _items = dirs + files;
105 | _loading = false;
106 | });
107 | }
108 |
109 | _handleTap(BrowseItem item) async {
110 | if (item.isDir) {
111 | BrowseResult? result = await Navigator.push(
112 | context,
113 | MaterialPageRoute(
114 | builder: (context) => FileBrowser(
115 | dir: item,
116 | isFave: widget.isFave,
117 | onToggleFave: widget.onToggleFave,
118 | settings: widget.settings,
119 | ),
120 | ),
121 | );
122 | if (result != null) {
123 | if (mounted) {
124 | Navigator.pop(context, result);
125 | }
126 | }
127 | } else {
128 | Navigator.pop(context, BrowseResult(item, BrowseResultIntent.play));
129 | }
130 | }
131 |
132 | @override
133 | Widget build(BuildContext context) {
134 | return Scaffold(
135 | appBar: AppBar(
136 | title: Text(widget.dir.title),
137 | actions: widget.dir.path != ''
138 | ? [
139 | IconButton(
140 | onPressed: () {
141 | setState(() {
142 | widget.onToggleFave(widget.dir);
143 | });
144 | },
145 | icon: Icon(
146 | widget.isFave(widget.dir) ? Icons.star : Icons.star_border,
147 | color: Colors.white,
148 | ),
149 | )
150 | ]
151 | : null,
152 | ),
153 | body: _renderList(),
154 | );
155 | }
156 |
157 | _renderList() {
158 | if (_loading) {
159 | return Center(
160 | child: Column(
161 | mainAxisAlignment: MainAxisAlignment.center,
162 | children: const [CircularProgressIndicator()],
163 | ),
164 | );
165 | }
166 |
167 | if (_errorMessage != null) {
168 | return Center(
169 | child: Column(
170 | mainAxisAlignment: MainAxisAlignment.center,
171 | children: [
172 | ListTile(
173 | leading: const Icon(
174 | Icons.error,
175 | color: Colors.redAccent,
176 | size: 48,
177 | ),
178 | title: Text(_errorMessage!),
179 | subtitle: Text(_errorDetail!),
180 | ),
181 | ],
182 | ),
183 | );
184 | }
185 |
186 | if (_items.isEmpty) {
187 | return Center(
188 | child: Column(
189 | mainAxisAlignment: MainAxisAlignment.center,
190 | children: const [Text('No compatible files found')],
191 | ),
192 | );
193 | }
194 |
195 | return Scrollbar(
196 | child: ListView.separated(
197 | itemCount: _items.length,
198 | itemBuilder: (context, i) {
199 | var item = _items[i];
200 | return EnqueueMenuGestureDetector(
201 | item: item,
202 | child: ListTile(
203 | dense: widget.settings.dense,
204 | leading: Icon(item.icon),
205 | title: Text(item.title),
206 | enabled: !_loading,
207 | onTap: () {
208 | _handleTap(item);
209 | },
210 | ),
211 | );
212 | },
213 | separatorBuilder: (context, i) {
214 | if (_items[i].isDir &&
215 | i < _items.length - 1 &&
216 | _items[i + 1].isFile) {
217 | return const Divider();
218 | }
219 | return const SizedBox.shrink();
220 | },
221 | ),
222 | );
223 | }
224 | }
225 |
--------------------------------------------------------------------------------
/lib/host_ip_guide.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | import 'models.dart';
4 | import 'widgets.dart';
5 |
6 | class HostIpGuide extends StatefulWidget {
7 | const HostIpGuide({Key? key}) : super(key: key);
8 |
9 | @override
10 | State createState() => _HostIpGuideState();
11 | }
12 |
13 | class _HostIpGuideState extends State {
14 | OperatingSystem? _os;
15 |
16 | _onOsChanged(os) {
17 | setState(() {
18 | _os = os;
19 | });
20 | }
21 |
22 | @override
23 | Widget build(BuildContext context) {
24 | return Scaffold(
25 | appBar: AppBar(
26 | centerTitle: true,
27 | title: Column(
28 | crossAxisAlignment: CrossAxisAlignment.end,
29 | children: [
30 | const Text('Host IP Guide'),
31 | if (_os != null)
32 | Text('for ${osNames[_os]}', style: const TextStyle(fontSize: 13)),
33 | ],
34 | ),
35 | ),
36 | body: buildBody(),
37 | );
38 | }
39 |
40 | Widget buildBody() {
41 | var theme = Theme.of(context);
42 | switch (_os) {
43 | case OperatingSystem.macos:
44 | return ListView(padding: const EdgeInsets.all(24), children: [
45 | TextAndImages(children: [
46 | const Text('From the Apple menu, select "System Preferences".'),
47 | const Text(
48 | 'In the System Preferences window, click on the "Network" item:'),
49 | Image.asset('assets/mac-network.png'),
50 | const Text(
51 | 'Your IP address will be visible to the right of the Network window, as shown below:'),
52 | Image.asset('assets/mac-ip.png'),
53 | ]),
54 | ]);
55 | case OperatingSystem.linux:
56 | return ListView(padding: const EdgeInsets.all(24), children: [
57 | TextAndImages(children: [
58 | const Text(
59 | 'Open a Terminal – on many popular distros this can be done by pressing Ctr + Alt + T.'),
60 | const Text(
61 | 'In the Terminal window, type "ifconfig" and press Enter:'),
62 | Image.asset('assets/linux-terminal.png'),
63 | const Text(
64 | 'Your host IP will be one of the "inet" IP results which appears in the output of the command:'),
65 | Image.asset('assets/linux-ifconfig.png'),
66 | const Text(
67 | 'Depending on how your computer is set up, there may be multiple results:'),
68 | const Text(
69 | 'If you connect to the network via an ethernet cable, look for the inet IP under an "eth0" interface.'),
70 | const Text(
71 | 'If you connect to the network via Wi-Fi, look for the inet IP under a "wlan0" interface.'),
72 | ]),
73 | ]);
74 | case OperatingSystem.windows:
75 | return ListView(padding: const EdgeInsets.all(24), children: [
76 | TextAndImages(children: [
77 | const Text(
78 | 'Open a Command Prompt by pressing Win + R, typing "cmd" in the dialog which appears, and presssing Enter:'),
79 | Image.asset('assets/windows-run.png'),
80 | const Text(
81 | 'In the Command Prompt window, type "ipconfig" and press Enter:'),
82 | Image.asset('assets/windows-cmd.png'),
83 | const Text(
84 | 'Your host IP will be one of the "IPv4 Address" results which appears in the output of the command:'),
85 | Image.asset('assets/windows-ipconfig.png'),
86 | const Text(
87 | 'Depending on how your computer is set up, there may be multiple results:'),
88 | const Text(
89 | 'If you connect to the network via an ethernet cable, look for the IPv4 Address under an "Ethernet adapter Ethernet" result.'),
90 | const Text(
91 | 'If you connect to the network via Wi-Fi, look for the IPv4 Address under an "Ethernet adapter Wireless Network Connection" result.'),
92 | ]),
93 | ]);
94 | default:
95 | return Column(
96 | children: [
97 | Padding(
98 | padding: const EdgeInsets.all(24),
99 | child: Wrap(
100 | runSpacing: 16,
101 | children: [
102 | Text('Which operating system are you running VLC on?',
103 | style: theme.textTheme.subtitle1),
104 | Column(
105 | children: OperatingSystem.values
106 | .map((os) => RadioListTile(
107 | title: Text(osNames[os]!),
108 | value: os,
109 | groupValue: _os,
110 | onChanged: _onOsChanged,
111 | ))
112 | .toList())
113 | ],
114 | ),
115 | ),
116 | ],
117 | );
118 | }
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/lib/main.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:flutter/services.dart';
5 | import 'package:shared_preferences/shared_preferences.dart';
6 |
7 | import 'models.dart';
8 | import 'remote_control.dart';
9 |
10 | /// Global access to the navigator state for showing an error dialog.
11 | final GlobalKey navigatorKey =
12 | GlobalKey(debugLabel: 'AppNavigator');
13 |
14 | /// Flag to avoid showing multiple error dialogs.
15 | var showingErrorDialog = false;
16 |
17 | void main() async {
18 | FlutterError.onError = (FlutterErrorDetails details) async {
19 | Zone.current.handleUncaughtError(details.exception, details.stack!);
20 | };
21 |
22 | runZonedGuarded>(() async {
23 | WidgetsFlutterBinding.ensureInitialized();
24 | var prefs = await SharedPreferences.getInstance();
25 | runApp(VlcRemote(prefs: prefs, settings: Settings(prefs)));
26 | }, (error, stackTrace) async {
27 | if (showingErrorDialog ||
28 | navigatorKey.currentState?.overlay?.context == null) {
29 | return;
30 | }
31 | showingErrorDialog = true;
32 | await showDialog(
33 | context: navigatorKey.currentState!.overlay!.context,
34 | builder: (context) => AlertDialog(
35 | title: const Text('Unhandled Error'),
36 | content: SingleChildScrollView(
37 | child: Column(
38 | children: [
39 | Text('$error\n\n$stackTrace'),
40 | ],
41 | ),
42 | ),
43 | actions: [
44 | TextButton(
45 | child: const Text('COPY ERROR DETAILS'),
46 | onPressed: () {
47 | Clipboard.setData(
48 | ClipboardData(text: '$error\n\n$stackTrace'),
49 | );
50 | },
51 | )
52 | ],
53 | ),
54 | );
55 | showingErrorDialog = false;
56 | });
57 | }
58 |
59 | class VlcRemote extends StatelessWidget {
60 | final SharedPreferences prefs;
61 | final Settings settings;
62 |
63 | const VlcRemote({
64 | Key? key,
65 | required this.prefs,
66 | required this.settings,
67 | }) : super(key: key);
68 |
69 | @override
70 | Widget build(BuildContext context) {
71 | return MaterialApp(
72 | navigatorKey: navigatorKey,
73 | title: 'Remote Control for VLC',
74 | theme: ThemeData(
75 | primarySwatch: Colors.deepOrange,
76 | ),
77 | home: RemoteControl(prefs: prefs, settings: settings),
78 | );
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/lib/models.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:shared_preferences/shared_preferences.dart';
5 | import 'package:xml/xml.dart' as xml;
6 |
7 | import 'utils.dart';
8 |
9 | var _videoExtensions = RegExp(
10 | r'\.(3g2|3gp|3gp2|3gpp|amv|asf|avi|divx|drc|dv|f4v|flv|gvi|gxf|ismv|iso|m1v|m2v|m2t|m2ts|m4v|mkv|mov|mp2|mp2v|mp4|mp4v|mpe|mpeg|mpeg1|mpeg2|mpeg4|mpg|mpv2|mts|mtv|mxf|mxg|nsv|nut|nuv|ogm|ogv|ogx|ps|rec|rm|rmvb|tod|ts|tts|vob|vro|webm|wm|wmv|wtv|xesc)$');
11 |
12 | var _audioExtensions = RegExp(
13 | r'\.(3ga|a52|aac|ac3|adt|adts|aif|aifc|aiff|alac|amr|aob|ape|awb|caf|dts|flac|it|m4a|m4b|m4p|mid|mka|mlp|mod|mpa|mp1|mp2|mp3|mpc|mpga|oga|ogg|oma|opus|ra|ram|rmi|s3m|spx|tta|voc|vqf|w64|wav|wma|wv|xa|xm)$');
14 |
15 | var _playlistExtensions = RegExp(
16 | r'\.(asx|b4s|cue|ifo|m3u|m3u8|pls|ram|rar|sdp|vlc|xspf|wax|wvx|zip|conf)');
17 |
18 | var _audioTranslations = RegExp(
19 | r"^(Audio|_Audio|Ameslaw|Aodio|Audioa|Audiu|Deng|Dźwięk|Ekirikuhurirwa|Endobozi|Fuaim|Fuaim|Garsas|Hang|Hljóð|Leo|Ljud|Lyd|M_adungan|Ma giwinyo|Odio|Ojoo|Oudio|Ovoz|Sain|Ses|Sonido|Səs|Umsindo|Zvok|Zvuk|Zëri|Àudio|Áudio|Ääni|Ήχος|Аудио|Аўдыё|Дуу|Дыбыс|Звук|Ձայն|שמע|آڈیو, صدا|ئۈن|آڈیو|دەنگ|صدا|غږيز|अडिअ'|अडियो|आडियो|ध्वनी|অডিঅ'|অডিও|ਆਡੀਓ|ઓડિયો|ଅଡ଼ିଓ|ஒலி|శ్రవ్యకం|ಧ್ವನಿ|ഓഡിയോ|ශ්රව්ය|เสียง|အသံ|აუდიო|ተሰሚ|ድምፅ|អូឌីយ៉ូ|オーディオ|音訊|音频|오디오)$");
20 |
21 | var _codecTranslations = RegExp(
22 | r"^(Codec|Bonez|Codifica|Codificador|Cudecu|Còdec|Códec|Dekko|Enkusike|i-Codec|Kodavimas|Kodek|Kodeka|Kodeks|Kodlayıcı/Çözücü|Koodek|Koodekki|Kóðalykill (codec)|Kôdek|Scéim Comhbhrúite|Кодек|Кодэк|Կոդեկ|מקודד/מפענח|كود يەشكۈچ|كوديك|کوڈیک|کوډېک|کُدک|کۆدێک|कोडेक|কোডেক|કોડેક|କୋଡେକ୍|கோடக்|కొడెక్|ಸಂಕೇತಕ|കോഡെക്ക്|කොඩෙක්|ตัวอ่าน-ลงรหัส|კოდეკი|ኮዴክ|កូដិក|コーデック|編解碼器|编解码器|코덱)$");
23 |
24 | var _descriptionTranslations = RegExp(
25 | r"^(Description|Apraksts|Aprašymas|Açıklama|Beschreibung|Beschrijving|Beskriuwing|Beskrivelse|Beskrivning|Beskrywing|Cifagol|Cur síos|Descrición|Descriere|Descripcion|Descripció|Descripción|Descrizion|Descrizione|Descrição|Deskribapena|Deskripsi|Deskrivadur|Discrijhaedje|Discrizzione|Disgrifiad|Ennyinyonyola|Enshoborora|Fa'amatalaga|Hedef|Incazelo|Keterangan|Kirjeldus|Kuvaus|Leírás|Lýsing|Mô tả|Opis|Popis|Përshkrimi|Skildring|Ta’rifi|Te lok|Tuairisgeul|Περιγραφή|Апісанне|Баяндама|Опис|Описание|Сипаттама|Тайлбар|Тасвирлама|Նկարագրություն|תיאור|الوصف|سپړاوی|شرح|وضاحت|پەسن|چۈشەندۈرۈش|बेखेवथि|वर्णन|विवरण|বর্ণনা|বিবরণ|বিৱৰণ|ਵੇਰਵਾ|વર્ણન|ବିବରଣୀ|விவரம்|వివరణ|ವಿವರಣೆ|വിവരണം|විස්තරය|รายละเอียด|ဖော်ပြချက်|აღწერილობა|መግለጫ|សេចក្ដីពណ៌នា|描述|說明|説明|설명)$");
26 |
27 | var _languageTranslations = RegExp(
28 | r"^(Language|Bahasa|Bahasa|Cànan|Dil|Gagana|Gjuha|Hizkuntza|Iaith|Idioma|Jazyk|Jezik|Kalba|Keel|Kieli|Langue|Leb|Lenga|Lenghe|Limbă|Lingaedje|Lingua|Llingua|Ngôn ngữ|Nyelv|Olulimi|Orurimi|Sprache|Språk|Taal|Teanga|Til|Tungumál|Ulimi|Valoda|Wybór języka|Yezh|Ziman|Ɗemngal|Γλώσσα|Език|Мова|Тел|Тил|Тілі|Хэл|Язык|Језик|Լեզու|שפה|اللغة|تىل|زبان|زمان|ژبه|भाषा|राव|ভাষা|ਭਾਸ਼ਾ|ભાષા|ଭାଷା|மொழி|భాష|ಭಾಷೆ|ഭാഷ|භාෂාව|ภาษา|ဘာသာစကား|ენა|ቋንቋ|ቋንቋ|ភាសា|言語|語言|语言|언어)$");
29 |
30 | var _subtitleTranslations = RegExp(
31 | r"^(Subtitle|Altyazı|Azpititulua|Binnivîs/OSD|Emitwe|Felirat|Fo-thiotal|Fotheideal|Gagana fa'aliliu|Isdeitlau|Istitl|Izihlokwana|Legenda|Legendas|Lestiitol|Napisy|Omutwe ogwokubiri|Onderskrif|Ondertitel|Phụ đề|Podnapisi|Podnaslov|Podtitl|Sarikata|Sortite|Sostítols|Sot titul|Sottotitoli|Sottutitulu|Sous-titres|Subtiiter|Subtitlu|Subtitol|Subtitr|Subtitrai|Subtitrs|Subtitulo|Subtítol|Subtítulo|Subtítulos|Subtítulu|Tekstitys|Terjemahan|Texti|Titra|Titulky|Titulky|Undertekst|Undertext|Undertitel|Υπότιτλος|Дэд бичвэр|Превод|Субтитр|Субтитри|Субтитрлер|Субтитры|Субтитрі|Субтытры|Титл|Ենթագիր|अनुवाद पट्टी|उपशीर्षक|दालाय-बिमुं|উপশিৰোনাম|বিকল্প নাম|সাবটাইটেল|ਸਬ-ਟਾਈਟਲ|ઉપશીર્ષક|ଉପଟାଇଟେଲ୍|துணை உரை|ఉపశీర్షిక|ಉಪಶೀರ್ಷಿಕೆ|ഉപശീര്ഷകം|උපසිරැසි|บทบรรยาย|စာတန်းထိုး|ტიტრები|ንዑስ አርእስት|ጽሁፋዊ ትርጉሞች|ចំណងជើងរង|字幕|자막)$");
32 |
33 | var _typeTranslations = RegExp(
34 | r"^(Type|Cineál|Cure|Ekyika|Fannu|Handiika|Itū'āiga|Jenis|Kite|Liik|Loại|Math|Mota|Rizh|Seòrsa|Sôre|Tegund|Tip|Tipas|Tipe|Tipi|Tipo|Tips|Tipu|Tipus|Turi|Typ|Typo|Tyyppi|Típus|Tür|Uhlobo|Vrsta|Τύπος|Врста|Тип|Түрі|Түрү|Төр|Төрөл|Տեսակ|סוג|تىپى|جۆر|نوع|ٹایِپ|ډول|टंकलेखन करा|टाइप|प्रकार|रोखोम|ধরন|প্রকার|প্ৰকাৰ|ਟਾਈਪ|પ્રકાર|ପ୍ରକାର|வகை|రకం|ಪ್ರಕಾರ|തരം|වර්ගය|ประเภท|အမျိုးအစား|ტიპი|አይነት|ប្រភេទ|タイプ|类型|類型|형식)$");
35 |
36 | enum OperatingSystem { linux, macos, windows }
37 |
38 | Map osNames = {
39 | OperatingSystem.linux: 'Linux',
40 | OperatingSystem.macos: 'macOS',
41 | OperatingSystem.windows: 'Windows',
42 | };
43 |
44 | class BrowseItem {
45 | String type, name, path, uri;
46 |
47 | BrowseItem(
48 | this.type,
49 | this.name,
50 | this.path,
51 | this.uri,
52 | );
53 |
54 | BrowseItem.fromUrl(String url)
55 | : uri = url.startsWith(urlRegExp) ? url : 'https://$url',
56 | type = 'web',
57 | path = '',
58 | name = '';
59 |
60 | /// Sending a directory: url when enqueueing makes a directory display as directory in the VLC
61 | /// playlist instead of as a generic file.
62 | String get playlistUri {
63 | if (isDir) return uri.replaceAll(RegExp(r'^file'), 'directory');
64 | return uri;
65 | }
66 |
67 | IconData get icon {
68 | if (isDir) {
69 | return Icons.folder;
70 | }
71 | if (isWeb) {
72 | return Icons.public;
73 | }
74 | if (isAudio) {
75 | return Icons.audiotrack;
76 | }
77 | if (isVideo) {
78 | return Icons.movie;
79 | }
80 | if (isPlaylist) {
81 | return Icons.list;
82 | }
83 | return Icons.insert_drive_file;
84 | }
85 |
86 | bool get isAudio => _audioExtensions.hasMatch(path);
87 |
88 | bool get isDir => type == 'dir';
89 |
90 | bool get isFile => type == 'file';
91 |
92 | bool get isPlaylist => _playlistExtensions.hasMatch(path);
93 |
94 | bool get isSupportedMedia => isAudio || isVideo || isWeb;
95 |
96 | bool get isVideo => _videoExtensions.hasMatch(path);
97 |
98 | bool get isWeb => type == 'web';
99 |
100 | String get title => isVideo ? cleanVideoTitle(name, keepExt: isFile) : name;
101 |
102 | BrowseItem.fromJson(Map json)
103 | : type = json['type'],
104 | name = json['name'],
105 | path = json['path'],
106 | uri = json['uri'];
107 |
108 | Map toJson() => {
109 | 'type': type,
110 | 'name': name,
111 | 'path': path,
112 | 'uri': uri,
113 | };
114 |
115 | @override
116 | String toString() => 'BrowseItem(${toJson()})';
117 | }
118 |
119 | enum BrowseResultIntent { play, enqueue }
120 |
121 | class BrowseResult {
122 | BrowseItem item;
123 | BrowseResultIntent intent;
124 | BrowseResult(this.item, this.intent);
125 | }
126 |
127 | // ignore: unused_element
128 | const _emulatorLocalhost = '10.0.2.2';
129 |
130 | const defaultPort = '8080';
131 | const defaultPassword = 'vlcplayer';
132 |
133 | var _ipPattern = RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$');
134 | var _numericPattern = RegExp(r'^\d+$');
135 |
136 | class Connection {
137 | late String _ip = '';
138 | late String _port = defaultPort;
139 | late String _password = defaultPassword;
140 |
141 | String? _ipError;
142 | String? _portError;
143 | String? _passwordError;
144 |
145 | Connection();
146 |
147 | String get ip => _ip;
148 | String get port => _port;
149 | String get password => _password;
150 | get ipError => _ipError;
151 | get portError => _portError;
152 | get passwordError => _passwordError;
153 |
154 | String get authority => '$_ip:$_port';
155 |
156 | /// The connection stored in [Settings] will only have an IP if it's been
157 | /// successfully tested.
158 | bool get hasIp => _ip.isNotEmpty;
159 |
160 | bool get isValid =>
161 | _ipError == null && _portError == null && _passwordError == null;
162 |
163 | bool get isNotValid => !isValid;
164 |
165 | set ip(String value) {
166 | if (value.trim().isEmpty) {
167 | _ipError = 'An IP address is required';
168 | } else if (!_ipPattern.hasMatch(value)) {
169 | _ipError = 'Must have 4 parts separated by periods';
170 | } else {
171 | _ipError = null;
172 | }
173 | _ip = value;
174 | }
175 |
176 | set port(String value) {
177 | _port = value;
178 | if (value.trim().isEmpty) {
179 | _portError = 'A port number is required';
180 | } else if (!_numericPattern.hasMatch(value)) {
181 | _portError = 'Must be all digits';
182 | } else {
183 | _portError = null;
184 | }
185 | _port = value;
186 | }
187 |
188 | set password(String value) {
189 | if (value.trim().isEmpty) {
190 | _passwordError = 'A password is required';
191 | } else {
192 | _passwordError = null;
193 | }
194 | _password = value;
195 | }
196 |
197 | Connection.fromJson(Map json) {
198 | ip = json['ip'] ?? '';
199 | port = json['port'] ?? defaultPort;
200 | password = json['password'] ?? defaultPassword;
201 | }
202 |
203 | Map toJson() => {
204 | 'ip': ip,
205 | 'port': port,
206 | 'password': password,
207 | };
208 | }
209 |
210 | class Settings {
211 | final SharedPreferences _prefs;
212 |
213 | late bool blurredCoverBg;
214 | late bool dense;
215 | late Connection connection;
216 |
217 | Settings(this._prefs) {
218 | Map json =
219 | jsonDecode(_prefs.getString('settings') ?? '{}');
220 | blurredCoverBg = json['blurredCoverBg'] ?? true;
221 | connection = Connection.fromJson(json['connection'] ?? {});
222 | dense = json['dense'] ?? false;
223 | }
224 |
225 | Map toJson() => {
226 | 'blurredCoverBg': blurredCoverBg,
227 | 'connection': connection,
228 | 'dense': dense,
229 | };
230 |
231 | save() {
232 | _prefs.setString('settings', jsonEncode(this));
233 | }
234 | }
235 |
236 | class LanguageTrack {
237 | String name;
238 | int streamNumber;
239 |
240 | LanguageTrack(this.name, this.streamNumber);
241 |
242 | @override
243 | String toString() {
244 | return '$name ($streamNumber)';
245 | }
246 | }
247 |
248 | class Equalizer {
249 | late bool enabled;
250 | late List presets;
251 | late List bands;
252 | late double preamp;
253 |
254 | @override
255 | String toString() {
256 | if (!enabled) {
257 | return 'Equalizer(off)';
258 | }
259 | return 'Equalizer(preamp: ${preamp.toStringAsFixed(1)}, bands: ${bands.map((b) => b.value.toStringAsFixed(1)).join(', ')})';
260 | }
261 | }
262 |
263 | class Band {
264 | int id;
265 | double value;
266 |
267 | Band(this.id, this.value);
268 | }
269 |
270 | class Preset {
271 | int id;
272 | String name;
273 |
274 | Preset(this.id, this.name);
275 | }
276 |
277 | String findFirstElementText(xml.XmlDocument document, String name,
278 | [String fallback = '']) {
279 | var elements = document.findAllElements(name);
280 | if (elements.isNotEmpty) {
281 | return elements.first.text;
282 | }
283 | return fallback;
284 | }
285 |
286 | String findFirstChildElementText(xml.XmlElement element, String name,
287 | [String fallback = '']) {
288 | var elements = element.findElements(name);
289 | if (elements.isNotEmpty) {
290 | return elements.first.text;
291 | }
292 | return fallback;
293 | }
294 |
295 | class VlcStatusResponse {
296 | xml.XmlDocument document;
297 |
298 | List? _audioTracks;
299 | List? _subtitleTracks;
300 | Map? _info;
301 |
302 | VlcStatusResponse(this.document);
303 |
304 | String get state => findFirstElementText(document, 'state');
305 |
306 | Duration get time =>
307 | Duration(seconds: int.parse(findFirstElementText(document, 'time', '0')));
308 |
309 | Duration get length => Duration(
310 | seconds: int.parse(findFirstElementText(document, 'length', '0')));
311 |
312 | int get volume {
313 | return int.parse(findFirstElementText(document, 'volume', '256'));
314 | }
315 |
316 | double get rate => double.parse(document.findAllElements('rate').first.text);
317 |
318 | Map get _metadata {
319 | if (_info == null) {
320 | xml.XmlElement? category;
321 | var informations = document.rootElement.findElements('information');
322 | if (informations.isNotEmpty) {
323 | var categories = informations.first.findElements('category');
324 | if (categories.isNotEmpty) {
325 | category = categories.first;
326 | }
327 | }
328 | _info = category != null
329 | ? {
330 | for (var el in category.findElements('info'))
331 | el.getAttribute('name') ?? '': el.text
332 | }
333 | : {};
334 | }
335 | return _info!;
336 | }
337 |
338 | String get title => _metadata['title'] ?? _metadata['filename'] ?? '';
339 |
340 | String get artist => _metadata['artist'] ?? '';
341 |
342 | String get artworkUrl => _metadata['artwork_url'] ?? '';
343 |
344 | bool get fullscreen => findFirstElementText(document, 'fullscreen') == 'true';
345 |
346 | bool get repeat => findFirstElementText(document, 'repeat') == 'true';
347 |
348 | bool get random => findFirstElementText(document, 'random') == 'true';
349 |
350 | bool get loop => findFirstElementText(document, 'loop') == 'true';
351 |
352 | String get currentPlId => findFirstElementText(document, 'currentplid', '-1');
353 |
354 | String get version => findFirstElementText(document, 'version');
355 |
356 | List get audioTracks {
357 | return _audioTracks ??= _getLanguageTracks(_audioTranslations);
358 | }
359 |
360 | List get subtitleTracks {
361 | return _subtitleTracks ??= _getLanguageTracks(_subtitleTranslations);
362 | }
363 |
364 | Equalizer get equalizer {
365 | var equalizer = Equalizer();
366 | var el = document.rootElement.findElements('equalizer').first;
367 | equalizer.enabled = el.firstChild != null;
368 | if (!equalizer.enabled) {
369 | return equalizer;
370 | }
371 | equalizer.presets = el
372 | .findAllElements('preset')
373 | .map((el) => Preset(
374 | int.parse(el.getAttribute('id')!),
375 | el.text,
376 | ))
377 | .toList();
378 | equalizer.presets.sort((a, b) => a.id - b.id);
379 | equalizer.bands = el
380 | .findAllElements('band')
381 | .map((el) => Band(
382 | int.parse(el.getAttribute('id')!),
383 | double.parse(el.text),
384 | ))
385 | .toList();
386 | equalizer.bands.sort((a, b) => a.id - b.id);
387 | equalizer.preamp = double.parse(el.findElements('preamp').first.text);
388 | return equalizer;
389 | }
390 |
391 | List _getLanguageTracks(RegExp type) {
392 | List tracks = [];
393 | document.findAllElements('category').forEach((category) {
394 | Map info = {
395 | for (var info in category.findElements('info'))
396 | info.getAttribute('name')!: info.text
397 | };
398 |
399 | String? typeKey = firstWhereOrNull(
400 | info.keys, (key) => _typeTranslations.hasMatch(key.trim()));
401 | if (typeKey == null || !type.hasMatch(info[typeKey]!.trim())) {
402 | return;
403 | }
404 |
405 | var streamName = category.getAttribute('name');
406 | var streamNumber = int.tryParse(streamName!.split(' ').last);
407 | if (streamNumber == null) {
408 | return;
409 | }
410 |
411 | var codec = _getStreamInfoItem(info, _codecTranslations);
412 | var description = _getStreamInfoItem(info, _descriptionTranslations);
413 | var language = _getStreamInfoItem(info, _languageTranslations);
414 |
415 | String name = streamName;
416 | if (description != null && language != null) {
417 | if (description.startsWith(language)) {
418 | name = description;
419 | } else {
420 | name = '$description [$language]';
421 | }
422 | } else if (language != null) {
423 | name = language;
424 | } else if (description != null) {
425 | name = description;
426 | } else if (codec != null) {
427 | name = codec;
428 | }
429 |
430 | tracks.add(LanguageTrack(name, streamNumber));
431 | });
432 | tracks.sort((a, b) => a.streamNumber - b.streamNumber);
433 | return tracks;
434 | }
435 |
436 | String? _getStreamInfoItem(Map info, RegExp name) {
437 | String? key =
438 | firstWhereOrNull(info.keys, (key) => name.hasMatch(key.trim()));
439 | return (key != null && info[key]!.isNotEmpty) ? info[key] : null;
440 | }
441 |
442 | @override
443 | String toString() {
444 | return 'VlcStatusResponse(${{
445 | 'state': state,
446 | 'time': time,
447 | 'length': length,
448 | 'volume': volume,
449 | 'title': title,
450 | 'fullscreen': fullscreen,
451 | 'repeat': repeat,
452 | 'random': random,
453 | 'loop': loop,
454 | 'currentPlId': currentPlId,
455 | 'version': version,
456 | 'audioTracks': audioTracks,
457 | 'subtitleTracks': subtitleTracks,
458 | }})';
459 | }
460 | }
461 |
462 | class PlaylistItem {
463 | String id;
464 | String name;
465 | String uri;
466 | Duration duration;
467 | bool current;
468 |
469 | PlaylistItem.fromXmlElement(xml.XmlElement el)
470 | : name = el.getAttribute('name')!,
471 | id = el.getAttribute('id')!,
472 | duration = Duration(seconds: int.parse(el.getAttribute('duration')!)),
473 | uri = el.getAttribute('uri')!,
474 | current = el.getAttribute('current') != null;
475 |
476 | IconData get icon {
477 | if (isDir) {
478 | return Icons.folder;
479 | }
480 | if (isWeb) {
481 | return Icons.public;
482 | }
483 | if (isAudio) {
484 | return Icons.audiotrack;
485 | }
486 | if (isVideo) {
487 | return Icons.movie;
488 | }
489 | return Icons.insert_drive_file;
490 | }
491 |
492 | bool get isAudio => _audioExtensions.hasMatch(uri);
493 |
494 | bool get isDir => uri.startsWith('directory:');
495 |
496 | bool get isFile => uri.startsWith('file:');
497 |
498 | bool get isMedia => isAudio || isVideo || isWeb;
499 |
500 | bool get isVideo => _videoExtensions.hasMatch(uri);
501 |
502 | bool get isWeb => uri.startsWith('http');
503 |
504 | String get title => isVideo ? cleanVideoTitle(name, keepExt: false) : name;
505 |
506 | @override
507 | String toString() {
508 | return 'PlaylistItem(${{
509 | 'name': name,
510 | 'title': title,
511 | 'id': id,
512 | 'duration': duration,
513 | 'uri': uri,
514 | 'current': current
515 | }})';
516 | }
517 | }
518 |
519 | class VlcPlaylistResponse {
520 | List items;
521 | PlaylistItem? currentItem;
522 |
523 | VlcPlaylistResponse.fromXmlDocument(xml.XmlDocument doc)
524 | : items = doc.rootElement
525 | .findElements('node')
526 | .first
527 | .findAllElements('leaf')
528 | .map((el) => PlaylistItem.fromXmlElement(el))
529 | .toList() {
530 | currentItem = firstWhereOrNull(items, (item) => item.current);
531 | }
532 |
533 | @override
534 | String toString() {
535 | return 'VlcPlaylistResponse(${{
536 | 'items': items,
537 | 'currentItem': currentItem
538 | }})';
539 | }
540 | }
541 |
--------------------------------------------------------------------------------
/lib/open_media.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:flutter/services.dart';
5 | import 'package:shared_preferences/shared_preferences.dart';
6 |
7 | import 'file_browser.dart';
8 | import 'models.dart';
9 | import 'utils.dart';
10 | import 'widgets.dart';
11 |
12 | var fileSystemItem = BrowseItem('dir', 'File System', '', 'file:///');
13 |
14 | /// Allow some commonly-used media URLs without protocols to pass for Copied URL
15 | /// See https://github.com/videolan/vlc/tree/master/share/lua/playlist
16 | var probablyMediaUrlRegExp = RegExp([
17 | r'(www\.)?dailymotion\.com/video/',
18 | r'(www\.)?soundcloud\.com/.+/.+',
19 | r'((www|gaming)\.)?youtube\.com/|youtu\.be/',
20 | r'(www\.)?vimeo\.com/(channels/.+/)?\d+|player\.vimeo\.com/',
21 | ].join('|'));
22 |
23 | var wwwRegexp = RegExp(r'www\.');
24 |
25 | class OpenMedia extends StatefulWidget {
26 | final SharedPreferences prefs;
27 | final Settings settings;
28 |
29 | const OpenMedia({
30 | Key? key,
31 | required this.prefs,
32 | required this.settings,
33 | }) : super(key: key);
34 |
35 | @override
36 | State createState() => _OpenMediaState();
37 | }
38 |
39 | class _OpenMediaState extends State with WidgetsBindingObserver {
40 | late List _faves;
41 | BrowseItem? _clipboardUrlItem;
42 | String? _otherUrl;
43 |
44 | @override
45 | initState() {
46 | _faves = (jsonDecode(widget.prefs.getString('faves') ?? '[]') as List)
47 | .map((obj) => BrowseItem.fromJson(obj))
48 | .toList();
49 | super.initState();
50 | WidgetsBinding.instance.addObserver(this);
51 | _checkClipboard();
52 | }
53 |
54 | @override
55 | void dispose() {
56 | WidgetsBinding.instance.removeObserver(this);
57 | super.dispose();
58 | }
59 |
60 | @override
61 | void didChangeAppLifecycleState(AppLifecycleState state) {
62 | if (state == AppLifecycleState.resumed) {
63 | _checkClipboard();
64 | }
65 | }
66 |
67 | _checkClipboard() async {
68 | BrowseItem? urlItem;
69 | var data = await Clipboard.getData(Clipboard.kTextPlain);
70 | if (data != null &&
71 | data.text != null &&
72 | (data.text!.startsWith(urlRegExp) ||
73 | data.text!.startsWith(probablyMediaUrlRegExp))) {
74 | urlItem = BrowseItem.fromUrl(data.text!);
75 | }
76 | setState(() {
77 | _clipboardUrlItem = urlItem;
78 | });
79 | }
80 |
81 | String get _displayUrl => _clipboardUrlItem!.uri
82 | .replaceFirst(urlRegExp, '')
83 | .replaceFirst(wwwRegexp, '');
84 |
85 | _handleOtherUrl(intent) {
86 | if (_otherUrl == null || _otherUrl!.isEmpty) {
87 | return;
88 | }
89 | Navigator.pop(
90 | context, BrowseResult(BrowseItem.fromUrl(_otherUrl!), intent));
91 | }
92 |
93 | bool _isFave(BrowseItem item) {
94 | return _faves.any((fave) => item.path == fave.path);
95 | }
96 |
97 | _toggleFave(BrowseItem item) {
98 | setState(() {
99 | var index = _faves.indexWhere((fave) => item.path == fave.path);
100 | if (index != -1) {
101 | _faves.removeAt(index);
102 | } else {
103 | _faves.add(item);
104 | }
105 | widget.prefs.setString('faves', jsonEncode(_faves));
106 | });
107 | }
108 |
109 | _selectFile(BrowseItem dir) async {
110 | BrowseResult? result = await Navigator.push(
111 | context,
112 | MaterialPageRoute(
113 | builder: (context) => FileBrowser(
114 | dir: dir,
115 | isFave: _isFave,
116 | onToggleFave: _toggleFave,
117 | settings: widget.settings,
118 | ),
119 | ),
120 | );
121 | if (result != null) {
122 | if (mounted) {
123 | Navigator.pop(context, result);
124 | }
125 | }
126 | }
127 |
128 | @override
129 | Widget build(BuildContext context) {
130 | List listItems = [
131 | ListTile(
132 | dense: widget.settings.dense,
133 | title: const Text('File System'),
134 | leading: const Icon(Icons.folder),
135 | onTap: () {
136 | _selectFile(fileSystemItem);
137 | },
138 | ),
139 | if (_clipboardUrlItem != null)
140 | EnqueueMenuGestureDetector(
141 | item: _clipboardUrlItem!,
142 | child: ListTile(
143 | dense: widget.settings.dense,
144 | title: const Text('Copied URL'),
145 | subtitle: Text(_displayUrl),
146 | leading: const Icon(Icons.public),
147 | onTap: () {
148 | Navigator.pop(context,
149 | BrowseResult(_clipboardUrlItem!, BrowseResultIntent.play));
150 | },
151 | ),
152 | ),
153 | ExpansionTile(
154 | leading: const Icon(Icons.public),
155 | title: Text('${_clipboardUrlItem != null ? 'Other ' : ''}URL'),
156 | children: [
157 | Padding(
158 | padding: const EdgeInsets.symmetric(horizontal: 16),
159 | child: Column(
160 | children: [
161 | TextFormField(
162 | keyboardType: TextInputType.url,
163 | decoration: const InputDecoration(
164 | border: OutlineInputBorder(),
165 | contentPadding: EdgeInsets.symmetric(horizontal: 12),
166 | ),
167 | onChanged: (url) {
168 | setState(() {
169 | _otherUrl = url;
170 | });
171 | },
172 | ),
173 | Row(
174 | children: [
175 | Expanded(
176 | child: ElevatedButton(
177 | child: const Text('Play'),
178 | onPressed: () =>
179 | _handleOtherUrl(BrowseResultIntent.play),
180 | ),
181 | ),
182 | const SizedBox(width: 8),
183 | Expanded(
184 | child: ElevatedButton(
185 | child: const Text('Enqueue'),
186 | onPressed: () =>
187 | _handleOtherUrl(BrowseResultIntent.enqueue),
188 | ),
189 | ),
190 | ],
191 | )
192 | ],
193 | ),
194 | )
195 | ],
196 | ),
197 | ];
198 |
199 | if (_faves.isNotEmpty) {
200 | listItems.addAll([
201 | const Divider(),
202 | ListTile(
203 | dense: widget.settings.dense,
204 | title: Text('Starred', style: Theme.of(context).textTheme.subtitle2),
205 | ),
206 | ]);
207 | listItems.addAll(_faves.map((item) => Dismissible(
208 | key: Key(item.path),
209 | background: const LeaveBehindView(),
210 | child: ListTile(
211 | dense: widget.settings.dense,
212 | title: Text(item.name),
213 | leading: const Icon(Icons.folder_special),
214 | onTap: () {
215 | _selectFile(item);
216 | },
217 | ),
218 | onDismissed: (direction) {
219 | _toggleFave(item);
220 | },
221 | )));
222 | }
223 |
224 | return Scaffold(
225 | appBar: AppBar(
226 | title: const Text('Open Media'),
227 | ),
228 | body: ListView(children: listItems),
229 | );
230 | }
231 | }
232 |
233 | class LeaveBehindView extends StatelessWidget {
234 | const LeaveBehindView({Key? key}) : super(key: key);
235 |
236 | @override
237 | Widget build(BuildContext context) {
238 | return Container(
239 | color: Colors.red,
240 | padding: const EdgeInsets.all(16.0),
241 | child: Row(
242 | children: const [
243 | Icon(Icons.delete, color: Colors.white),
244 | Expanded(
245 | child: Text(''),
246 | ),
247 | Icon(Icons.delete, color: Colors.white),
248 | ],
249 | ),
250 | );
251 | }
252 | }
253 |
--------------------------------------------------------------------------------
/lib/settings_screen.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'dart:convert';
3 | import 'dart:io';
4 |
5 | import 'package:connectivity_plus/connectivity_plus.dart';
6 | import 'package:flutter/material.dart';
7 | import 'package:flutter/services.dart';
8 | import 'package:http/http.dart' as http;
9 | import 'package:network_info_plus/network_info_plus.dart';
10 |
11 | import 'host_ip_guide.dart';
12 | import 'models.dart';
13 | import 'widgets.dart';
14 |
15 | class SettingsScreen extends StatefulWidget {
16 | final Settings settings;
17 | final Function onSettingsChanged;
18 |
19 | const SettingsScreen(
20 | {Key? key, required this.settings, required this.onSettingsChanged})
21 | : super(key: key);
22 |
23 | @override
24 | State createState() => _SettingsScreenState();
25 | }
26 |
27 | class _SettingsScreenState extends State {
28 | Connection connection = Connection();
29 | var ipController = TextEditingController();
30 | var ipFocus = FocusNode();
31 | bool ipDirty = false;
32 | var portController = TextEditingController();
33 | var portFocus = FocusNode();
34 | bool portDirty = false;
35 | var passwordController = TextEditingController();
36 | var passwordFocus = FocusNode();
37 | bool passwordDirty = false;
38 |
39 | String? prefilledIpSuffix;
40 | bool showPassword = false;
41 |
42 | bool scanningNetwork = false;
43 |
44 | bool testingConnection = false;
45 | String? connectionTestResult;
46 | String? connectionTestResultDescription;
47 | IconData? connectionTestResultIcon;
48 |
49 | @override
50 | initState() {
51 | ipController.addListener(() {
52 | setState(() {
53 | connection.ip = ipController.text;
54 | });
55 | });
56 | portController.addListener(() {
57 | setState(() {
58 | connection.port = portController.text;
59 | });
60 | });
61 | passwordController.addListener(() {
62 | setState(() {
63 | connection.password = passwordController.text;
64 | });
65 | });
66 |
67 | ipController.text = widget.settings.connection.ip;
68 | portController.text = widget.settings.connection.port;
69 | passwordController.text = widget.settings.connection.password;
70 |
71 | ipFocus.addListener(() {
72 | if (!ipFocus.hasFocus) {
73 | setState(() {
74 | ipDirty = true;
75 | });
76 | }
77 | });
78 | portFocus.addListener(() {
79 | if (!portFocus.hasFocus) {
80 | setState(() {
81 | portDirty = true;
82 | });
83 | }
84 | });
85 | passwordFocus.addListener(() {
86 | if (!passwordFocus.hasFocus) {
87 | setState(() {
88 | passwordDirty = true;
89 | });
90 | }
91 | });
92 |
93 | super.initState();
94 |
95 | if (widget.settings.connection.ip == '') {
96 | _defaultIpPrefix();
97 | }
98 | }
99 |
100 | @override
101 | dispose() {
102 | ipController.dispose();
103 | ipFocus.dispose();
104 | portController.dispose();
105 | portFocus.dispose();
106 | passwordController.dispose();
107 | passwordFocus.dispose();
108 | super.dispose();
109 | }
110 |
111 | _defaultIpPrefix() async {
112 | if (await Connectivity().checkConnectivity() == ConnectivityResult.wifi) {
113 | var ip = await NetworkInfo().getWifiIP();
114 | if (ip != null) {
115 | setState(() {
116 | prefilledIpSuffix = ip.substring(0, ip.lastIndexOf('.') + 1);
117 | ipController.text = prefilledIpSuffix!;
118 | });
119 | }
120 | }
121 | }
122 |
123 | _testConnection() async {
124 | removeCurrentFocus(context);
125 | if (!connection.isValid) {
126 | setState(() {
127 | ipDirty = true;
128 | portDirty = true;
129 | passwordDirty = true;
130 | });
131 | return;
132 | }
133 |
134 | setState(() {
135 | connectionTestResult = null;
136 | connectionTestResultIcon = null;
137 | connectionTestResultDescription = null;
138 | testingConnection = true;
139 | });
140 |
141 | String result;
142 | String description;
143 | IconData icon;
144 |
145 | try {
146 | var response = await http.get(
147 | Uri.http(
148 | '${ipController.text}:${portController.text}',
149 | '/requests/status.xml',
150 | ),
151 | headers: {
152 | 'Authorization':
153 | 'Basic ${base64Encode(utf8.encode(':${passwordController.text}'))}'
154 | }).timeout(const Duration(seconds: 2));
155 |
156 | if (response.statusCode == 200) {
157 | widget.settings.connection = connection;
158 | widget.onSettingsChanged();
159 | result = 'Connection successful';
160 | description = 'Connection settings saved';
161 | icon = Icons.check;
162 | } else {
163 | icon = Icons.error;
164 | if (response.statusCode == 401) {
165 | result = 'Password is invalid';
166 | description = 'Tap the eye icon to check your password';
167 | } else {
168 | result = 'Unexpected response';
169 | description = 'Status code: ${response.statusCode}';
170 | }
171 | }
172 | } catch (e) {
173 | description = 'Check the IP and port settings';
174 | icon = Icons.error;
175 | if (e is TimeoutException) {
176 | result = 'Connection timed out';
177 | } else if (e is SocketException) {
178 | result = 'Connection error';
179 | } else {
180 | result = 'Connection error: ${e.runtimeType}';
181 | }
182 | }
183 |
184 | setState(() {
185 | connectionTestResult = result;
186 | connectionTestResultDescription = description;
187 | connectionTestResultIcon = icon;
188 | testingConnection = false;
189 | });
190 | }
191 |
192 | @override
193 | Widget build(BuildContext context) {
194 | final theme = Theme.of(context);
195 | final headingStyle = theme.textTheme.subtitle1!
196 | .copyWith(fontWeight: FontWeight.bold, color: theme.primaryColor);
197 | return Scaffold(
198 | appBar: AppBar(
199 | centerTitle: true,
200 | title: const Text('Settings'),
201 | ),
202 | body: ListView(children: [
203 | ListTile(
204 | dense: widget.settings.dense,
205 | title: Text(
206 | 'VLC connection',
207 | style: headingStyle,
208 | ),
209 | ),
210 | ListTile(
211 | dense: widget.settings.dense,
212 | title: TextField(
213 | controller: ipController,
214 | focusNode: ipFocus,
215 | keyboardType: const TextInputType.numberWithOptions(decimal: true),
216 | inputFormatters: [ipWhitelistingTextInputFormatter],
217 | decoration: InputDecoration(
218 | isDense: widget.settings.dense,
219 | icon: const Icon(Icons.computer),
220 | labelText: 'Host IP',
221 | errorText: ipDirty ? connection.ipError : null,
222 | helperText: prefilledIpSuffix != null &&
223 | connection.ip == prefilledIpSuffix
224 | ? 'Suffix pre-filled from your Wi-Fi IP'
225 | : null,
226 | ),
227 | ),
228 | trailing: IconButton(
229 | onPressed: () {
230 | Navigator.push(context,
231 | MaterialPageRoute(builder: (context) => const HostIpGuide()));
232 | },
233 | tooltip: 'Get help finding your IP',
234 | icon: Icon(Icons.help, color: theme.primaryColor),
235 | ),
236 | ),
237 | ListTile(
238 | dense: widget.settings.dense,
239 | title: TextField(
240 | controller: passwordController,
241 | focusNode: passwordFocus,
242 | obscureText: !showPassword,
243 | decoration: InputDecoration(
244 | isDense: widget.settings.dense,
245 | icon: const Icon(Icons.vpn_key),
246 | labelText: 'Password',
247 | errorText: passwordDirty ? connection.passwordError : null,
248 | ),
249 | ),
250 | trailing: IconButton(
251 | onPressed: () {
252 | setState(() {
253 | showPassword = !showPassword;
254 | });
255 | },
256 | tooltip: 'Toggle password visibility',
257 | icon: Icon(Icons.remove_red_eye,
258 | color: showPassword ? theme.primaryColor : null),
259 | ),
260 | ),
261 | ListTile(
262 | dense: widget.settings.dense,
263 | title: TextField(
264 | controller: portController,
265 | focusNode: portFocus,
266 | keyboardType: TextInputType.number,
267 | inputFormatters: [FilteringTextInputFormatter.digitsOnly],
268 | decoration: InputDecoration(
269 | isDense: widget.settings.dense,
270 | icon: const Icon(Icons.input),
271 | labelText: 'Port (default: 8080)',
272 | errorText: portDirty ? connection.portError : null,
273 | helperText: 'Advanced use only'),
274 | ),
275 | ),
276 | ListTile(
277 | dense: widget.settings.dense,
278 | title: ElevatedButton(
279 | style: ElevatedButton.styleFrom(
280 | primary: theme.buttonTheme.colorScheme!.primary,
281 | onPrimary: Colors.white,
282 | ),
283 | onPressed: !testingConnection ? _testConnection : null,
284 | child: Row(
285 | mainAxisSize: MainAxisSize.min,
286 | children: [
287 | testingConnection
288 | ? const Padding(
289 | padding: EdgeInsets.all(4.0),
290 | child: SizedBox(
291 | width: 16,
292 | height: 16,
293 | child: CircularProgressIndicator(
294 | valueColor:
295 | AlwaysStoppedAnimation(Colors.white)),
296 | ),
297 | )
298 | : const Icon(Icons.network_check),
299 | const SizedBox(width: 8.0),
300 | const Text('Test & Save Connection'),
301 | ],
302 | ),
303 | ),
304 | ),
305 | if (connectionTestResult != null)
306 | ListTile(
307 | dense: widget.settings.dense,
308 | leading: Icon(
309 | connectionTestResultIcon,
310 | color: connectionTestResultIcon == Icons.check
311 | ? Colors.green
312 | : Colors.redAccent,
313 | ),
314 | title: Text(connectionTestResult!),
315 | subtitle: connectionTestResultDescription != null
316 | ? Text(connectionTestResultDescription!)
317 | : null,
318 | ),
319 | const Divider(),
320 | ListTile(
321 | dense: widget.settings.dense,
322 | title: Text(
323 | 'Display options',
324 | style: headingStyle,
325 | ),
326 | ),
327 | CheckboxListTile(
328 | title: const Text('Compact display'),
329 | value: widget.settings.dense,
330 | dense: widget.settings.dense,
331 | onChanged: (dense) {
332 | setState(() {
333 | widget.settings.dense = dense ?? false;
334 | widget.onSettingsChanged();
335 | });
336 | },
337 | ),
338 | CheckboxListTile(
339 | title: const Text('Blurred cover background'),
340 | subtitle: const Text('When available for audio files'),
341 | value: widget.settings.blurredCoverBg,
342 | dense: widget.settings.dense,
343 | onChanged: (dense) {
344 | setState(() {
345 | widget.settings.blurredCoverBg = dense ?? false;
346 | widget.onSettingsChanged();
347 | });
348 | },
349 | ),
350 | ]),
351 | );
352 | }
353 | }
354 |
--------------------------------------------------------------------------------
/lib/utils.dart:
--------------------------------------------------------------------------------
1 | var _dot = RegExp(r'\.');
2 |
3 | var _episode = RegExp(r's\d\de\d\d', caseSensitive: false);
4 |
5 | // From https://en.wikipedia.org/wiki/Pirated_movie_release_types#Common_abbreviations
6 | var _source = [
7 | 'ABC',
8 | 'ATVP',
9 | 'AMZN',
10 | 'BBC',
11 | 'CBS',
12 | 'CC',
13 | 'CR',
14 | 'CW',
15 | 'DCU',
16 | 'DSNY',
17 | 'FBWatch',
18 | 'FREE',
19 | 'FOX',
20 | 'HULU',
21 | 'iP',
22 | 'LIFE',
23 | 'MTV',
24 | 'NBC',
25 | 'NICK',
26 | 'NF',
27 | 'RED',
28 | 'TF1',
29 | 'STZ',
30 | ].map((s) => RegExp.escape(s)).join('|');
31 |
32 | // From https://en.wikipedia.org/wiki/Pirated_movie_release_types#Release_formats
33 | var _format = [
34 | 'CAMRip',
35 | 'CAM',
36 | 'HDCAM',
37 | 'TS',
38 | 'HDTS',
39 | 'TELESYNC',
40 | 'PDVD',
41 | 'PreDVDRip',
42 | 'WP',
43 | 'WORKPRINT',
44 | 'TC',
45 | 'HDTC',
46 | 'TELECINE',
47 | 'PPV',
48 | 'PPVRip',
49 | 'SCR',
50 | 'SCREENER',
51 | 'DVDSCR',
52 | 'DVDSCREENER',
53 | 'BDSCR',
54 | 'DDC',
55 | 'R5',
56 | 'R5.LINE',
57 | 'R5.AC3.5.1.HQ',
58 | 'DVDRip',
59 | 'DVDMux',
60 | 'DVDR',
61 | 'DVD-Full',
62 | 'Full-Rip',
63 | 'ISO rip',
64 | 'lossless rip',
65 | 'untouched rip',
66 | 'DVD-5',
67 | 'DVD-9',
68 | 'DSR',
69 | 'DSRip',
70 | 'SATRip',
71 | 'DTHRip',
72 | 'DVBRip',
73 | 'HDTV',
74 | 'PDTV',
75 | 'DTVRip',
76 | 'TVRip',
77 | 'HDTVRip',
78 | 'VODRip',
79 | 'VODR',
80 | 'WEBDL',
81 | 'WEB DL',
82 | 'WEB-DL',
83 | 'HDRip',
84 | 'WEB-DLRip',
85 | 'WEBRip',
86 | 'WEB Rip',
87 | 'WEB-Rip',
88 | 'WEB',
89 | 'WEB-Cap',
90 | 'WEBCAP',
91 | 'WEB Cap',
92 | 'HC',
93 | 'HD-Rip',
94 | 'Blu-Ray',
95 | 'BluRay',
96 | 'BDRip',
97 | 'BRip',
98 | 'BRRip',
99 | 'BDMV',
100 | 'BDR',
101 | 'BD25',
102 | 'BD50',
103 | 'BD5',
104 | 'BD9',
105 | ].map((s) => RegExp.escape(s)).join('|');
106 |
107 | var _year = r'\d{4}';
108 |
109 | // 720p, 1080p etc.
110 | var _res = r'\d{3,4}p?';
111 |
112 | // DUBBED, JAPANESE, INDONESIAN etc.
113 | var _language = r'[A-Z]+';
114 |
115 | var _movie = RegExp(
116 | '\\.$_year(\\.$_language)?(\\.$_res)?(\\.($_source))?\\.($_format)',
117 | caseSensitive: false,
118 | );
119 |
120 | String cleanVideoTitle(String name, {bool keepExt = false}) {
121 | if (name == '') {
122 | return '';
123 | }
124 | if (_episode.hasMatch(name)) {
125 | return dotsToSpaces(name.substring(0, _episode.firstMatch(name)!.end));
126 | }
127 | if (_movie.hasMatch(name)) {
128 | return dotsToSpaces(name.substring(0, _movie.firstMatch(name)!.start));
129 | }
130 | return dotsToSpaces(name, keepExt: keepExt);
131 | }
132 |
133 | String dotsToSpaces(String s, {bool keepExt = false}) {
134 | String ext = '';
135 | var parts = s.split(_dot);
136 | if (keepExt) {
137 | ext = parts.removeLast();
138 | }
139 | return '${parts.join(' ')}.$ext';
140 | }
141 |
142 | /// [Iterable.firstWhere] doesn't work with null safety when you want to fall
143 | /// back to returning `null`.
144 | ///
145 | /// See https://github.com/dart-lang/sdk/issues/42947
146 | T? firstWhereOrNull(Iterable iterable, bool Function(T element) test) {
147 | for (var element in iterable) {
148 | if (test(element)) return element;
149 | }
150 | return null;
151 | }
152 |
153 | String formatTime(Duration duration) {
154 | String minutes = (duration.inMinutes % 60).toString().padLeft(2, '0');
155 | String seconds = (duration.inSeconds % 60).toString().padLeft(2, '0');
156 | return '${duration.inHours >= 1 ? '${duration.inHours}:' : ''}$minutes:$seconds';
157 | }
158 |
159 | /// Matches some of the protocols supported by VLC.
160 | var urlRegExp = RegExp(r'((f|ht)tps?|mms|rts?p)://');
161 |
162 | /*
163 | * Trick stolen from https://gist.github.com/shubhamjain/9809108#file-vlc_http-L108
164 | * The interface expects value between 0 and 512 while in the UI it is 0% to 200%.
165 | * So a factor of 2.56 is used to convert 0% to 200% to a scale of 0 to 512.
166 | */
167 | const volumeSliderScaleFactor = 2.56;
168 |
--------------------------------------------------------------------------------
/lib/vlc_configuration_guide.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | import 'models.dart';
4 | import 'widgets.dart';
5 |
6 | class VlcConfigurationGuide extends StatefulWidget {
7 | const VlcConfigurationGuide({Key? key}) : super(key: key);
8 |
9 | @override
10 | State createState() => _VlcConfigurationGuideState();
11 | }
12 |
13 | class _VlcConfigurationGuideState extends State {
14 | int _currentStep = 0;
15 | OperatingSystem? _os;
16 |
17 | _onOsChanged(os) {
18 | setState(() {
19 | _os = os;
20 | });
21 | }
22 |
23 | @override
24 | Widget build(BuildContext context) {
25 | return Scaffold(
26 | appBar: AppBar(
27 | centerTitle: true,
28 | title: Column(
29 | crossAxisAlignment: CrossAxisAlignment.end,
30 | children: [
31 | const Text('VLC Configuration Guide'),
32 | if (_os != null)
33 | Text('for ${osNames[_os]}', style: const TextStyle(fontSize: 13)),
34 | ],
35 | ),
36 | ),
37 | body: buildBody(),
38 | );
39 | }
40 |
41 | Widget buildBody() {
42 | switch (_os) {
43 | case OperatingSystem.macos:
44 | return buildMacStepper();
45 | case OperatingSystem.linux:
46 | case OperatingSystem.windows:
47 | return buildLinuxWindowsStepper();
48 | default:
49 | var theme = Theme.of(context);
50 | return Column(
51 | children: [
52 | Padding(
53 | padding: const EdgeInsets.all(24),
54 | child: Wrap(
55 | runSpacing: 16,
56 | children: [
57 | Text('Which operating system are you running VLC on?',
58 | style: theme.textTheme.subtitle1),
59 | Column(
60 | children: OperatingSystem.values
61 | .map((os) => RadioListTile(
62 | title: Text(osNames[os]!),
63 | value: os,
64 | groupValue: _os,
65 | onChanged: _onOsChanged,
66 | ))
67 | .toList())
68 | ],
69 | ),
70 | ),
71 | ],
72 | );
73 | }
74 | }
75 |
76 | Widget buildLinuxWindowsStepper() {
77 | var theme = Theme.of(context);
78 | var os = _os.toString().split('.').last;
79 | return Stepper(
80 | currentStep: _currentStep,
81 | controlsBuilder: (BuildContext context, ControlsDetails details) {
82 | return Padding(
83 | padding: const EdgeInsets.only(top: 16),
84 | child: Row(
85 | children: [
86 | TextButton(
87 | style: TextButton.styleFrom(
88 | backgroundColor: theme.primaryColor,
89 | primary: Colors.white,
90 | ),
91 | onPressed: details.onStepContinue,
92 | child: Text(_currentStep == 2 ? 'FINISHED' : 'NEXT STEP'),
93 | ),
94 | TextButton(
95 | onPressed: details.onStepCancel,
96 | child: const Text('PREVIOUS STEP'),
97 | ),
98 | ],
99 | ),
100 | );
101 | },
102 | onStepCancel: () {
103 | if (_currentStep > 0) {
104 | setState(() {
105 | _currentStep--;
106 | });
107 | } else {
108 | _onOsChanged(null);
109 | }
110 | },
111 | onStepContinue: () {
112 | if (_currentStep == 2) {
113 | Navigator.pop(context);
114 | return;
115 | }
116 | setState(() {
117 | _currentStep++;
118 | });
119 | },
120 | steps: [
121 | Step(
122 | title: const Text('Enable VLC\'s web interface'),
123 | content: TextAndImages(
124 | children: [
125 | const Text(
126 | 'In VLC\'s menu bar, select Tools > Preferences to open the preferences window:'),
127 | Image.asset('assets/$os-menu.png'),
128 | const Text(
129 | 'Switch to Advanced Preferences mode by clicking the "All" radio button in the "Show settings" section at the bottom left of the window:'),
130 | Image.asset('assets/$os-show-settings.png'),
131 | const Text(
132 | 'Scroll down to find the "Main interfaces" section and click it:'),
133 | Image.asset('assets/$os-main-interface.png'),
134 | const Text(
135 | 'Check the "Web" checkbox in the "Extra interface modules" section to enable the web interface:'),
136 | Image.asset('assets/$os-web.png'),
137 | ],
138 | ),
139 | ),
140 | Step(
141 | title: const Text('Set web interface password'),
142 | content: TextAndImages(
143 | children: [
144 | const Text(
145 | 'Expand the "Main interfaces" section by clicking the ">" chevron and click the "Lua" section which appears:'),
146 | Image.asset('assets/$os-lua.png'),
147 | const Text('Set a password in the "Lua HTTP" section:'),
148 | Image.asset('assets/$os-password.png'),
149 | const Text(
150 | 'Remote Control for VLC uses the password "vlcplayer" (without quotes) by default – if you set something else you\'ll have to manually configure the VLC connection.'),
151 | const Text('Finally, click Save to save your changes.'),
152 | ],
153 | ),
154 | ),
155 | Step(
156 | title: const Text('Close and restart VLC'),
157 | content: Row(
158 | children: const [
159 | Text('Close and restart VLC to activate the web interface.'),
160 | ],
161 | ),
162 | )
163 | ],
164 | );
165 | }
166 |
167 | Widget buildMacStepper() {
168 | var theme = Theme.of(context);
169 | return Stepper(
170 | currentStep: _currentStep,
171 | controlsBuilder: (BuildContext context, ControlsDetails details) {
172 | return Padding(
173 | padding: const EdgeInsets.only(top: 16),
174 | child: Row(
175 | children: [
176 | TextButton(
177 | style: TextButton.styleFrom(
178 | backgroundColor: theme.primaryColor,
179 | primary: Colors.white,
180 | ),
181 | onPressed: details.onStepContinue,
182 | child: Text(_currentStep == 1 ? 'FINISHED' : 'NEXT STEP'),
183 | ),
184 | TextButton(
185 | onPressed: details.onStepCancel,
186 | child: const Text('PREVIOUS STEP'),
187 | ),
188 | ],
189 | ),
190 | );
191 | },
192 | onStepCancel: () {
193 | if (_currentStep > 0) {
194 | setState(() {
195 | _currentStep--;
196 | });
197 | } else {
198 | _onOsChanged(null);
199 | }
200 | },
201 | onStepContinue: () {
202 | if (_currentStep == 1) {
203 | Navigator.pop(context);
204 | return;
205 | }
206 | setState(() {
207 | _currentStep++;
208 | });
209 | },
210 | steps: [
211 | Step(
212 | title: const Text('Enable VLC\'s web interface'),
213 | content: TextAndImages(
214 | children: [
215 | const Text(
216 | 'In the Menubar, select VLC > Preferences to open the preferences window:'),
217 | Image.asset('assets/mac-menu.png'),
218 | const Text(
219 | 'At the bottom of the "Interface" settings page, check "Enable HTTP web interface" and set a password.'),
220 | Image.asset('assets/mac-http-interface.png'),
221 | const Text(
222 | 'Remote Control for VLC uses the password "vlcplayer" (without quotes) by default – if you set something else you\'ll have to manually configure the VLC connection.'),
223 | const Text('Finally, click Save to save your changes.'),
224 | ],
225 | ),
226 | ),
227 | Step(
228 | title: const Text('Quit and restart VLC'),
229 | content: Row(
230 | children: const [
231 | Text('Quit and restart VLC to activate the web interface.'),
232 | ],
233 | ),
234 | )
235 | ],
236 | );
237 | }
238 | }
239 |
--------------------------------------------------------------------------------
/lib/widgets.dart:
--------------------------------------------------------------------------------
1 | import 'dart:ui' as ui;
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:flutter/services.dart';
5 |
6 | import 'models.dart';
7 |
8 | class EnqueueMenuGestureDetector extends StatefulWidget {
9 | final Widget child;
10 | final BrowseItem item;
11 |
12 | const EnqueueMenuGestureDetector(
13 | {Key? key, required this.child, required this.item})
14 | : super(key: key);
15 |
16 | @override
17 | State createState() =>
18 | _EnqueueMenuGestureDetectorState();
19 | }
20 |
21 | class _EnqueueMenuGestureDetectorState
22 | extends State {
23 | late Offset _tapPosition;
24 |
25 | _handleTapDown(details) {
26 | _tapPosition = details.globalPosition;
27 | }
28 |
29 | _showMenu() async {
30 | final Size size = Overlay.of(context)!.context.size!;
31 | var intent = await showMenu(
32 | context: context,
33 | items: >[
34 | const PopupMenuItem(
35 | value: BrowseResultIntent.play,
36 | child: Text('Play'),
37 | ),
38 | const PopupMenuItem(
39 | value: BrowseResultIntent.enqueue,
40 | child: Text('Enqueue'),
41 | ),
42 | ],
43 | position: RelativeRect.fromRect(
44 | _tapPosition & const Size(40, 40), Offset.zero & size),
45 | );
46 | if (intent != null) {
47 | if (mounted) {
48 | Navigator.pop(context, BrowseResult(widget.item, intent));
49 | }
50 | }
51 | }
52 |
53 | @override
54 | Widget build(BuildContext context) {
55 | return GestureDetector(
56 | onTapDown: _handleTapDown,
57 | onLongPress: _showMenu,
58 | child: widget.child,
59 | );
60 | }
61 | }
62 |
63 | /// A custom track shape for a [Slider] which lets it go full-width.
64 | ///
65 | /// From https://github.com/flutter/flutter/issues/37057#issuecomment-516048356
66 | class FullWidthTrackShape extends RoundedRectSliderTrackShape {
67 | @override
68 | Rect getPreferredRect({
69 | required RenderBox parentBox,
70 | Offset offset = Offset.zero,
71 | required SliderThemeData sliderTheme,
72 | bool isEnabled = false,
73 | bool isDiscrete = false,
74 | }) {
75 | final double trackHeight = sliderTheme.trackHeight!;
76 | final double trackLeft = offset.dx;
77 | final double trackTop =
78 | offset.dy + (parentBox.size.height - trackHeight) / 2;
79 | final double trackWidth = parentBox.size.width;
80 | return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight);
81 | }
82 | }
83 |
84 | var _intlStrings = {
85 | 'Equalizer': 'Equaliser',
86 | 'Settings': 'Settings',
87 | };
88 |
89 | String intl(String enUsString) {
90 | if (ui.window.locale.countryCode == 'US' ||
91 | !_intlStrings.containsKey(enUsString)) {
92 | return enUsString;
93 | }
94 | return _intlStrings[enUsString]!;
95 | }
96 |
97 | /// Like [Iterable.join] but for lists of Widgets.
98 | Iterable intersperseWidgets(Iterable iterable,
99 | {required Widget Function() builder}) sync* {
100 | final iterator = iterable.iterator;
101 | if (iterator.moveNext()) {
102 | yield iterator.current;
103 | while (iterator.moveNext()) {
104 | yield builder();
105 | yield iterator.current;
106 | }
107 | }
108 | }
109 |
110 | /// A [WhitelistingTextInputFormatter] that takes in digits `[0-9]` and periods
111 | /// `.` only.
112 | var ipWhitelistingTextInputFormatter =
113 | FilteringTextInputFormatter.allow(RegExp(r'[\d.]+'));
114 |
115 | /// Remove current focus to hide the keyboard.
116 | removeCurrentFocus(BuildContext context) {
117 | FocusScopeNode currentFocus = FocusScope.of(context);
118 | if (!currentFocus.hasPrimaryFocus) {
119 | currentFocus.unfocus();
120 | }
121 | }
122 |
123 | class TextAndImages extends StatelessWidget {
124 | final List children;
125 | final double spacing;
126 |
127 | const TextAndImages({Key? key, required this.children, this.spacing = 16})
128 | : super(key: key);
129 |
130 | @override
131 | Widget build(BuildContext context) {
132 | return Column(
133 | children: intersperseWidgets(
134 | children.map((child) => Row(children: [
135 | Expanded(
136 | child: Container(
137 | alignment: Alignment.topLeft,
138 | child: child,
139 | ))
140 | ])),
141 | builder: () => SizedBox(height: spacing),
142 | ).toList(),
143 | );
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/pubspec.lock:
--------------------------------------------------------------------------------
1 | # Generated by pub
2 | # See https://dart.dev/tools/pub/glossary#lockfile
3 | packages:
4 | args:
5 | dependency: transitive
6 | description:
7 | name: args
8 | url: "https://pub.dartlang.org"
9 | source: hosted
10 | version: "2.3.1"
11 | async:
12 | dependency: transitive
13 | description:
14 | name: async
15 | url: "https://pub.dartlang.org"
16 | source: hosted
17 | version: "2.8.2"
18 | boolean_selector:
19 | dependency: transitive
20 | description:
21 | name: boolean_selector
22 | url: "https://pub.dartlang.org"
23 | source: hosted
24 | version: "2.1.0"
25 | characters:
26 | dependency: transitive
27 | description:
28 | name: characters
29 | url: "https://pub.dartlang.org"
30 | source: hosted
31 | version: "1.2.0"
32 | charcode:
33 | dependency: transitive
34 | description:
35 | name: charcode
36 | url: "https://pub.dartlang.org"
37 | source: hosted
38 | version: "1.3.1"
39 | clock:
40 | dependency: transitive
41 | description:
42 | name: clock
43 | url: "https://pub.dartlang.org"
44 | source: hosted
45 | version: "1.1.0"
46 | collection:
47 | dependency: transitive
48 | description:
49 | name: collection
50 | url: "https://pub.dartlang.org"
51 | source: hosted
52 | version: "1.16.0"
53 | connectivity_plus:
54 | dependency: "direct main"
55 | description:
56 | name: connectivity_plus
57 | url: "https://pub.dartlang.org"
58 | source: hosted
59 | version: "2.3.0"
60 | connectivity_plus_linux:
61 | dependency: transitive
62 | description:
63 | name: connectivity_plus_linux
64 | url: "https://pub.dartlang.org"
65 | source: hosted
66 | version: "1.3.0"
67 | connectivity_plus_macos:
68 | dependency: transitive
69 | description:
70 | name: connectivity_plus_macos
71 | url: "https://pub.dartlang.org"
72 | source: hosted
73 | version: "1.2.2"
74 | connectivity_plus_platform_interface:
75 | dependency: transitive
76 | description:
77 | name: connectivity_plus_platform_interface
78 | url: "https://pub.dartlang.org"
79 | source: hosted
80 | version: "1.2.0"
81 | connectivity_plus_web:
82 | dependency: transitive
83 | description:
84 | name: connectivity_plus_web
85 | url: "https://pub.dartlang.org"
86 | source: hosted
87 | version: "1.2.0"
88 | connectivity_plus_windows:
89 | dependency: transitive
90 | description:
91 | name: connectivity_plus_windows
92 | url: "https://pub.dartlang.org"
93 | source: hosted
94 | version: "1.2.0"
95 | crypto:
96 | dependency: transitive
97 | description:
98 | name: crypto
99 | url: "https://pub.dartlang.org"
100 | source: hosted
101 | version: "3.0.2"
102 | dart_ping:
103 | dependency: transitive
104 | description:
105 | name: dart_ping
106 | url: "https://pub.dartlang.org"
107 | source: hosted
108 | version: "6.1.2"
109 | dbus:
110 | dependency: transitive
111 | description:
112 | name: dbus
113 | url: "https://pub.dartlang.org"
114 | source: hosted
115 | version: "0.7.3"
116 | fake_async:
117 | dependency: transitive
118 | description:
119 | name: fake_async
120 | url: "https://pub.dartlang.org"
121 | source: hosted
122 | version: "1.3.0"
123 | ffi:
124 | dependency: transitive
125 | description:
126 | name: ffi
127 | url: "https://pub.dartlang.org"
128 | source: hosted
129 | version: "1.1.2"
130 | file:
131 | dependency: transitive
132 | description:
133 | name: file
134 | url: "https://pub.dartlang.org"
135 | source: hosted
136 | version: "6.1.2"
137 | flutter:
138 | dependency: "direct main"
139 | description: flutter
140 | source: sdk
141 | version: "0.0.0"
142 | flutter_lints:
143 | dependency: "direct dev"
144 | description:
145 | name: flutter_lints
146 | url: "https://pub.dartlang.org"
147 | source: hosted
148 | version: "2.0.1"
149 | flutter_test:
150 | dependency: "direct dev"
151 | description: flutter
152 | source: sdk
153 | version: "0.0.0"
154 | flutter_web_plugins:
155 | dependency: transitive
156 | description: flutter
157 | source: sdk
158 | version: "0.0.0"
159 | http:
160 | dependency: "direct main"
161 | description:
162 | name: http
163 | url: "https://pub.dartlang.org"
164 | source: hosted
165 | version: "0.13.4"
166 | http_parser:
167 | dependency: transitive
168 | description:
169 | name: http_parser
170 | url: "https://pub.dartlang.org"
171 | source: hosted
172 | version: "4.0.0"
173 | intl:
174 | dependency: transitive
175 | description:
176 | name: intl
177 | url: "https://pub.dartlang.org"
178 | source: hosted
179 | version: "0.17.0"
180 | js:
181 | dependency: transitive
182 | description:
183 | name: js
184 | url: "https://pub.dartlang.org"
185 | source: hosted
186 | version: "0.6.4"
187 | just_throttle_it:
188 | dependency: "direct main"
189 | description:
190 | name: just_throttle_it
191 | url: "https://pub.dartlang.org"
192 | source: hosted
193 | version: "2.1.0"
194 | lints:
195 | dependency: transitive
196 | description:
197 | name: lints
198 | url: "https://pub.dartlang.org"
199 | source: hosted
200 | version: "2.0.0"
201 | logging:
202 | dependency: transitive
203 | description:
204 | name: logging
205 | url: "https://pub.dartlang.org"
206 | source: hosted
207 | version: "1.0.2"
208 | matcher:
209 | dependency: transitive
210 | description:
211 | name: matcher
212 | url: "https://pub.dartlang.org"
213 | source: hosted
214 | version: "0.12.11"
215 | material_color_utilities:
216 | dependency: transitive
217 | description:
218 | name: material_color_utilities
219 | url: "https://pub.dartlang.org"
220 | source: hosted
221 | version: "0.1.4"
222 | meta:
223 | dependency: transitive
224 | description:
225 | name: meta
226 | url: "https://pub.dartlang.org"
227 | source: hosted
228 | version: "1.7.0"
229 | network_info_plus:
230 | dependency: "direct main"
231 | description:
232 | name: network_info_plus
233 | url: "https://pub.dartlang.org"
234 | source: hosted
235 | version: "2.1.3"
236 | network_info_plus_linux:
237 | dependency: transitive
238 | description:
239 | name: network_info_plus_linux
240 | url: "https://pub.dartlang.org"
241 | source: hosted
242 | version: "1.1.2"
243 | network_info_plus_macos:
244 | dependency: transitive
245 | description:
246 | name: network_info_plus_macos
247 | url: "https://pub.dartlang.org"
248 | source: hosted
249 | version: "1.3.0"
250 | network_info_plus_platform_interface:
251 | dependency: transitive
252 | description:
253 | name: network_info_plus_platform_interface
254 | url: "https://pub.dartlang.org"
255 | source: hosted
256 | version: "1.1.2"
257 | network_info_plus_web:
258 | dependency: transitive
259 | description:
260 | name: network_info_plus_web
261 | url: "https://pub.dartlang.org"
262 | source: hosted
263 | version: "1.0.1"
264 | network_info_plus_windows:
265 | dependency: transitive
266 | description:
267 | name: network_info_plus_windows
268 | url: "https://pub.dartlang.org"
269 | source: hosted
270 | version: "1.0.2"
271 | network_tools:
272 | dependency: "direct main"
273 | description:
274 | name: network_tools
275 | url: "https://pub.dartlang.org"
276 | source: hosted
277 | version: "1.0.8"
278 | nm:
279 | dependency: transitive
280 | description:
281 | name: nm
282 | url: "https://pub.dartlang.org"
283 | source: hosted
284 | version: "0.5.0"
285 | path:
286 | dependency: transitive
287 | description:
288 | name: path
289 | url: "https://pub.dartlang.org"
290 | source: hosted
291 | version: "1.8.1"
292 | path_provider_linux:
293 | dependency: transitive
294 | description:
295 | name: path_provider_linux
296 | url: "https://pub.dartlang.org"
297 | source: hosted
298 | version: "2.1.6"
299 | path_provider_platform_interface:
300 | dependency: transitive
301 | description:
302 | name: path_provider_platform_interface
303 | url: "https://pub.dartlang.org"
304 | source: hosted
305 | version: "2.0.4"
306 | path_provider_windows:
307 | dependency: transitive
308 | description:
309 | name: path_provider_windows
310 | url: "https://pub.dartlang.org"
311 | source: hosted
312 | version: "2.0.6"
313 | petitparser:
314 | dependency: transitive
315 | description:
316 | name: petitparser
317 | url: "https://pub.dartlang.org"
318 | source: hosted
319 | version: "5.0.0"
320 | platform:
321 | dependency: transitive
322 | description:
323 | name: platform
324 | url: "https://pub.dartlang.org"
325 | source: hosted
326 | version: "3.1.0"
327 | plugin_platform_interface:
328 | dependency: transitive
329 | description:
330 | name: plugin_platform_interface
331 | url: "https://pub.dartlang.org"
332 | source: hosted
333 | version: "2.1.2"
334 | process:
335 | dependency: transitive
336 | description:
337 | name: process
338 | url: "https://pub.dartlang.org"
339 | source: hosted
340 | version: "4.2.4"
341 | shared_preferences:
342 | dependency: "direct main"
343 | description:
344 | name: shared_preferences
345 | url: "https://pub.dartlang.org"
346 | source: hosted
347 | version: "2.0.15"
348 | shared_preferences_android:
349 | dependency: transitive
350 | description:
351 | name: shared_preferences_android
352 | url: "https://pub.dartlang.org"
353 | source: hosted
354 | version: "2.0.12"
355 | shared_preferences_ios:
356 | dependency: transitive
357 | description:
358 | name: shared_preferences_ios
359 | url: "https://pub.dartlang.org"
360 | source: hosted
361 | version: "2.1.1"
362 | shared_preferences_linux:
363 | dependency: transitive
364 | description:
365 | name: shared_preferences_linux
366 | url: "https://pub.dartlang.org"
367 | source: hosted
368 | version: "2.1.1"
369 | shared_preferences_macos:
370 | dependency: transitive
371 | description:
372 | name: shared_preferences_macos
373 | url: "https://pub.dartlang.org"
374 | source: hosted
375 | version: "2.0.4"
376 | shared_preferences_platform_interface:
377 | dependency: transitive
378 | description:
379 | name: shared_preferences_platform_interface
380 | url: "https://pub.dartlang.org"
381 | source: hosted
382 | version: "2.0.0"
383 | shared_preferences_web:
384 | dependency: transitive
385 | description:
386 | name: shared_preferences_web
387 | url: "https://pub.dartlang.org"
388 | source: hosted
389 | version: "2.0.4"
390 | shared_preferences_windows:
391 | dependency: transitive
392 | description:
393 | name: shared_preferences_windows
394 | url: "https://pub.dartlang.org"
395 | source: hosted
396 | version: "2.1.1"
397 | sky_engine:
398 | dependency: transitive
399 | description: flutter
400 | source: sdk
401 | version: "0.0.99"
402 | source_span:
403 | dependency: transitive
404 | description:
405 | name: source_span
406 | url: "https://pub.dartlang.org"
407 | source: hosted
408 | version: "1.8.2"
409 | stack_trace:
410 | dependency: transitive
411 | description:
412 | name: stack_trace
413 | url: "https://pub.dartlang.org"
414 | source: hosted
415 | version: "1.10.0"
416 | stream_channel:
417 | dependency: transitive
418 | description:
419 | name: stream_channel
420 | url: "https://pub.dartlang.org"
421 | source: hosted
422 | version: "2.1.0"
423 | string_scanner:
424 | dependency: transitive
425 | description:
426 | name: string_scanner
427 | url: "https://pub.dartlang.org"
428 | source: hosted
429 | version: "1.1.0"
430 | term_glyph:
431 | dependency: transitive
432 | description:
433 | name: term_glyph
434 | url: "https://pub.dartlang.org"
435 | source: hosted
436 | version: "1.2.0"
437 | test_api:
438 | dependency: transitive
439 | description:
440 | name: test_api
441 | url: "https://pub.dartlang.org"
442 | source: hosted
443 | version: "0.4.9"
444 | transparent_image:
445 | dependency: "direct main"
446 | description:
447 | name: transparent_image
448 | url: "https://pub.dartlang.org"
449 | source: hosted
450 | version: "2.0.0"
451 | typed_data:
452 | dependency: transitive
453 | description:
454 | name: typed_data
455 | url: "https://pub.dartlang.org"
456 | source: hosted
457 | version: "1.3.0"
458 | universal_io:
459 | dependency: transitive
460 | description:
461 | name: universal_io
462 | url: "https://pub.dartlang.org"
463 | source: hosted
464 | version: "2.0.4"
465 | vector_math:
466 | dependency: transitive
467 | description:
468 | name: vector_math
469 | url: "https://pub.dartlang.org"
470 | source: hosted
471 | version: "2.1.2"
472 | win32:
473 | dependency: transitive
474 | description:
475 | name: win32
476 | url: "https://pub.dartlang.org"
477 | source: hosted
478 | version: "2.5.2"
479 | xdg_directories:
480 | dependency: transitive
481 | description:
482 | name: xdg_directories
483 | url: "https://pub.dartlang.org"
484 | source: hosted
485 | version: "0.2.0+1"
486 | xml:
487 | dependency: "direct main"
488 | description:
489 | name: xml
490 | url: "https://pub.dartlang.org"
491 | source: hosted
492 | version: "5.4.1"
493 | sdks:
494 | dart: ">=2.17.0 <3.0.0"
495 | flutter: ">=2.8.0"
496 |
--------------------------------------------------------------------------------
/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: remote_control_for_vlc
2 | description: Remote Control for VLC
3 |
4 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev
5 |
6 | version: 1.5.0+15
7 |
8 | environment:
9 | sdk: ">=2.17.0 <3.0.0"
10 |
11 | dependencies:
12 | flutter:
13 | sdk: flutter
14 | connectivity_plus: ^2.3.0
15 | http: ^0.13.4
16 | just_throttle_it: ^2.1.0
17 | network_info_plus: ^2.1.3
18 | network_tools: ^1.0.8
19 | shared_preferences: ^2.0.15
20 | transparent_image: ^2.0.0
21 | xml: ^5.4.1
22 |
23 | dev_dependencies:
24 | flutter_test:
25 | sdk: flutter
26 |
27 | # The "flutter_lints" package below contains a set of recommended lints to
28 | # encourage good coding practices. The lint set provided by the package is
29 | # activated in the `analysis_options.yaml` file located at the root of your
30 | # package. See that file for information about deactivating specific lint
31 | # rules and activating additional ones.
32 | flutter_lints: ^2.0.0
33 |
34 | # For information on the generic Dart part of this file, see the
35 | # following page: https://dart.dev/tools/pub/pubspec
36 |
37 | flutter:
38 | uses-material-design: true
39 |
40 | assets:
41 | - assets/icon-512.png
42 | - assets/cone.png
43 | - assets/linux-ifconfig.png
44 | - assets/linux-lua.png
45 | - assets/linux-main-interface.png
46 | - assets/linux-menu.png
47 | - assets/linux-password.png
48 | - assets/linux-show-settings.png
49 | - assets/linux-terminal.png
50 | - assets/linux-web.png
51 | - assets/mac-http-interface.png
52 | - assets/mac-ip.png
53 | - assets/mac-menu.png
54 | - assets/mac-network.png
55 | - assets/signal-0.png
56 | - assets/signal-1.png
57 | - assets/signal-2.png
58 | - assets/signal-3.png
59 | - assets/windows-cmd.png
60 | - assets/windows-ipconfig.png
61 | - assets/windows-lua.png
62 | - assets/windows-main-interface.png
63 | - assets/windows-menu.png
64 | - assets/windows-password.png
65 | - assets/windows-run.png
66 | - assets/windows-show-settings.png
67 | - assets/windows-web.png
68 |
--------------------------------------------------------------------------------
/screenshots/equalizer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/screenshots/equalizer.png
--------------------------------------------------------------------------------
/screenshots/file-browser.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/screenshots/file-browser.png
--------------------------------------------------------------------------------
/screenshots/host-ip-guide-os.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/screenshots/host-ip-guide-os.png
--------------------------------------------------------------------------------
/screenshots/open-media.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/screenshots/open-media.png
--------------------------------------------------------------------------------
/screenshots/playback-speed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/screenshots/playback-speed.png
--------------------------------------------------------------------------------
/screenshots/playing-menu-vlc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/screenshots/playing-menu-vlc.png
--------------------------------------------------------------------------------
/screenshots/playing-vlc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/screenshots/playing-vlc.png
--------------------------------------------------------------------------------
/screenshots/settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/screenshots/settings.png
--------------------------------------------------------------------------------
/screenshots/setup-guide-os.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/screenshots/setup-guide-os.png
--------------------------------------------------------------------------------
/screenshots/setup-guide-steps.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/screenshots/setup-guide-steps.png
--------------------------------------------------------------------------------
/screenshots/setup.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/screenshots/setup.png
--------------------------------------------------------------------------------
/screenshots/vlc-connected.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/insin/remote_control_for_vlc/ec2fcee9e023ccdfbf43e5807612d73f3e39727d/screenshots/vlc-connected.png
--------------------------------------------------------------------------------