Initial commit
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
*.g.dart linguist-generated=true
|
||||
*.freezed.dart linguist-generated=true
|
||||
*.sh text eol=lf
|
||||
|
||||
android/app/libs/*.aar binary -text linguist-generated=true
|
||||
|
||||
linux/flutter/generated_plugin_registrant.cc text eol=lf
|
||||
linux/flutter/generated_plugins.cmake text eol=lf
|
||||
macos/Flutter/GeneratedPluginRegistrant.swift text eol=lf
|
||||
windows/flutter/generated_plugin_registrant.cc text eol=lf
|
||||
windows/flutter/generated_plugins.cmake text eol=lf
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.build/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
.swiftpm/
|
||||
migrate_working_dir/
|
||||
|
||||
# Local editor/tool state
|
||||
.idea/
|
||||
.cursor/
|
||||
*.iml
|
||||
.ace-tool/
|
||||
.vscode/
|
||||
.claude/index.json
|
||||
.claude/settings.local.json
|
||||
.claude/worktrees/
|
||||
.codex/config.toml
|
||||
.takumi/
|
||||
.workflow/
|
||||
.codestable/
|
||||
/.omc/
|
||||
*.moc
|
||||
node_modules/
|
||||
out/
|
||||
devtools_options.yaml
|
||||
|
||||
# Screenshots & debug captures
|
||||
flutter_*.png
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/.gradle/
|
||||
**/doc/api/
|
||||
**/ios/Flutter/.last_build_id
|
||||
.dart_tool/
|
||||
.flutter-plugins-dependencies
|
||||
.pub-cache/
|
||||
.pub/
|
||||
build/
|
||||
coverage/
|
||||
|
||||
# Platform generated artifacts
|
||||
**/generated_plugin_registrant.*
|
||||
**/generated_plugins.cmake
|
||||
linux/flutter/ephemeral/
|
||||
macos/Flutter/ephemeral/
|
||||
macos/Pods/
|
||||
windows/flutter/ephemeral/
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
# Obfuscation related
|
||||
app.*.map.json
|
||||
|
||||
# Android Studio will place build artifacts here
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
|
||||
# Android machine-specific / generated (do not commit)
|
||||
/android/local.properties
|
||||
**/GeneratedPluginRegistrant.*
|
||||
|
||||
# Large binaries & build artifacts (prevent re-commit)
|
||||
.media3-build/
|
||||
dist/
|
||||
**/target/
|
||||
*.pdb
|
||||
*.rlib
|
||||
*.rmeta
|
||||
*.dll
|
||||
*.dylib
|
||||
*.so
|
||||
*.7z
|
||||
*.wasm
|
||||
test_link/
|
||||
|
||||
# Local git worktrees
|
||||
.worktrees/
|
||||
|
||||
# Superpowers brainstorm visual companion
|
||||
.superpowers/
|
||||
|
||||
# Opencode project config may be committed; dependency/runtime output should not.
|
||||
.opencode/node_modules/
|
||||
.opencode/images/
|
||||
.opencode/.cache/
|
||||
|
||||
# Windows reserved device name (created accidentally)
|
||||
nul
|
||||
|
||||
# 本地签名密钥,勿提交
|
||||
android/release-signing.json
|
||||
@@ -0,0 +1,30 @@
|
||||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "02085feb3f5d8a8156e5e28512b9d99351d510c0"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: 02085feb3f5d8a8156e5e28512b9d99351d510c0
|
||||
base_revision: 02085feb3f5d8a8156e5e28512b9d99351d510c0
|
||||
- platform: android
|
||||
create_revision: 02085feb3f5d8a8156e5e28512b9d99351d510c0
|
||||
base_revision: 02085feb3f5d8a8156e5e28512b9d99351d510c0
|
||||
|
||||
# User provided section
|
||||
|
||||
# List of Local paths (relative to this file) that should be
|
||||
# ignored by the migrate tool.
|
||||
#
|
||||
# Files that are not part of the templates will be ignored by default.
|
||||
unmanaged_files:
|
||||
- 'lib/main.dart'
|
||||
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||
@@ -0,0 +1,185 @@
|
||||
# SMPlayer
|
||||
|
||||
一个 Emby 媒体服务器跨平台客户端。
|
||||
|
||||
目前主要面向 Windows / macOS
|
||||
## 主要功能
|
||||
|
||||
- Emby 媒体库浏览:主页、媒体库、详情、搜索、收藏、播放记录、最近添加。
|
||||
- 播放器:倍速、音轨 / 字幕切换、字幕样式、画面比例、选集、全屏、桌面画中画。
|
||||
- 弹幕:兼容弹弹 play / danmu_api 风格 API,支持多源、自动匹配、手动搜索、过滤和样式调整。
|
||||
- 多服务器:可添加多个 Emby 服务器,支持本地备份 / 导入配置。
|
||||
- 扩展功能:可选 TMDB 详情 / 发现页、Trakt scrobble 和应用内更新。
|
||||
|
||||
## 技术栈
|
||||
|
||||
Flutter、Riverpod、Dio / Retrofit、fvp / libmdk、media_kit / mpv、media3 / ExoPlayer + FFmpeg(Android)、canvas_danmaku。
|
||||
|
||||
## 播放引擎
|
||||
|
||||
播放器内置三个可切换的解码 / 渲染引擎,可在"设置 → 播放设置 → 播放引擎"中选择:
|
||||
|
||||
| 引擎 | 适用平台 | 解码路径 | 说明 |
|
||||
|------|----------|----------|------|
|
||||
| fvp (libmdk) | Windows / macOS / Android | 平台硬解 + libmdk 软解兜底 | `auto` 模式的主引擎;杜比视界 / ProRes 优先走它 |
|
||||
| mpv (media_kit) | Windows / macOS / Android | mpv / libmpv | 兼容性次引擎;桌面 `auto` 模式备选 |
|
||||
| media3 (ExoPlayer + FFmpeg) | Android only | MediaCodec 硬解视频 + FFmpeg 软解音频 | 专为补 Android 高规格音轨(DTS / TrueHD / EAC3);需自编译 AAR,见 `plugins/media3_engine/README.md` 与 `scripts/build_media3_ffmpeg.sh` |
|
||||
|
||||
`auto` 模式在 Android 上默认 fvp、桌面默认 mpv;遇 DV / ProRes 强制 fvp。media3 仅在用户手动选中时启用,且桌面平台不会展示该选项(运行时也有路由层守卫降级到 mpv)。
|
||||
|
||||
## 下载
|
||||
|
||||
前往 GitHub Releases 页面下载发布版本(需自行配置仓库地址)。
|
||||
|
||||
当前自动发布流程主要提供 Windows x64 安装包。macOS 和 Android 可按下方开发说明自行构建。
|
||||
|
||||
## 项目结构
|
||||
|
||||
- `lib/core` - 合约、领域用例、仓储端口、存储和 Emby API 适配器。
|
||||
- `lib/features` - 首页、媒体库、详情、播放、搜索、收藏、设置等功能页面。
|
||||
- `lib/providers` - Riverpod 状态和依赖注入。
|
||||
- `lib/shared` - 可复用组件、主题和映射工具。
|
||||
- `macos`, `windows` - Flutter 桌面平台外壳。
|
||||
- `android` - Flutter Android 平台外壳。
|
||||
- `installer/`, `scripts/` - Windows Inno Setup 脚本和开发 / 发布辅助脚本。
|
||||
- `test` - Flutter 测试。
|
||||
|
||||
## 开发
|
||||
|
||||
### Script entry (`package.json`, npm-style)
|
||||
|
||||
If you prefer `npm run <task>` over remembering each Flutter/PowerShell invocation, a thin
|
||||
`package.json` maps familiar names onto the existing scripts. It is `private` (no publish,
|
||||
no `node_modules` needed) — npm is only used as a task runner; the single source of truth
|
||||
stays in `scripts/*.ps1`.
|
||||
|
||||
```sh
|
||||
npm run dev # -> scripts/dev.ps1 run (Windows smart run)
|
||||
npm run dev:macos # -> flutter run -d macos
|
||||
npm run gen # -> build_runner build --delete-conflicting-outputs
|
||||
npm run watch # -> build_runner watch --delete-conflicting-outputs
|
||||
npm run build:windows # -> flutter build windows --release
|
||||
npm run build:macos # -> flutter build macos --release
|
||||
npm run build:apk # -> flutter build apk --release
|
||||
npm run test # -> flutter test
|
||||
npm run analyze # -> flutter analyze
|
||||
npm run clean # -> flutter clean
|
||||
npm run pub # -> flutter pub get
|
||||
npm run doctor # -> flutter doctor -v
|
||||
npm run installer # -> scripts/dev.ps1 installer (Windows .exe)
|
||||
npm run dmg # -> scripts/build_dmg.sh (macOS .dmg)
|
||||
```
|
||||
|
||||
Pass-through args use the npm `--` separator; everything after it is forwarded to the
|
||||
underlying script. To force `pub get` and run in release mode:
|
||||
|
||||
```sh
|
||||
npm run dev -- -Force -FlutterArgs --release # -> scripts/dev.ps1 run -Force -FlutterArgs --release
|
||||
```
|
||||
|
||||
`dev`/`installer` shell out to PowerShell (Windows); `dmg` needs bash (macOS). The rest are
|
||||
plain Flutter CLI calls and work on any platform.
|
||||
|
||||
### Windows — unified entry (`scripts/dev.ps1`)
|
||||
|
||||
One-click subcommand dispatcher. Delegates to `dev-run.ps1` / `build-installer.ps1` and wraps common Flutter tasks. On Windows, `run` prepares the project-pinned fvp/libmdk SDK and clears `FVP_DEPS_LATEST`, so local development never silently switches to a nightly dependency.
|
||||
|
||||
```powershell
|
||||
# Smart dev run (skips `pub get` when unchanged)
|
||||
powershell -ExecutionPolicy Bypass -File scripts\dev.ps1 run
|
||||
|
||||
# Force a clean re-download of the project-pinned fvp/mdk-sdk:
|
||||
powershell -ExecutionPolicy Bypass -File scripts\dev.ps1 run -RefreshFvpDeps
|
||||
|
||||
# Build .exe installer (Inno Setup 6 required, see below)
|
||||
powershell -ExecutionPolicy Bypass -File scripts\dev.ps1 installer
|
||||
powershell -ExecutionPolicy Bypass -File scripts\dev.ps1 installer -SkipFlutterBuild
|
||||
powershell -ExecutionPolicy Bypass -File scripts\dev.ps1 installer -Version 1.0.1
|
||||
|
||||
# Other shortcuts
|
||||
scripts\dev.ps1 build # flutter build windows --release
|
||||
scripts\dev.ps1 gen # dart run build_runner build --delete-conflicting-outputs
|
||||
scripts\dev.ps1 clean # flutter clean
|
||||
scripts\dev.ps1 pub # flutter pub get
|
||||
scripts\dev.ps1 analyze # flutter analyze
|
||||
scripts\dev.ps1 doctor # flutter doctor -v
|
||||
scripts\dev.ps1 help # full subcommand list
|
||||
```
|
||||
|
||||
Pass-through args:
|
||||
- `dev.ps1 run -Force -FlutterArgs --release` → forces `pub get` and runs Flutter in release mode.
|
||||
- `dev.ps1 run -Device macos` → choose a different target.
|
||||
- `dev.ps1 run -RefreshFvpDeps` → deletes the cached SDK/archive and re-downloads the project-pinned fvp/MDK version.
|
||||
|
||||
Avoid using this for daily Windows development:
|
||||
|
||||
```powershell
|
||||
$env:FVP_DEPS_LATEST=1; flutter run -d windows
|
||||
```
|
||||
|
||||
Use `scripts\dev.ps1 run` instead; it also skips `pub get` when dependencies are unchanged.
|
||||
|
||||
### macOS / Android
|
||||
|
||||
Use the Flutter CLI directly:
|
||||
|
||||
```sh
|
||||
flutter pub get
|
||||
flutter run -d macos # macOS
|
||||
npm run dev:android # Android (auto-detect emulator or device)
|
||||
flutter build macos # macOS bundle
|
||||
npm run build:apk # Android APK with project-pinned MDK
|
||||
./scripts/build_dmg.sh # macOS .dmg packaging
|
||||
```
|
||||
|
||||
> **Android 构建前置 — Media3 FFmpeg decoder AAR**:Android 端的 media3 引擎依赖
|
||||
> 自编译的 `media3-decoder-ffmpeg-1.10.1-arm64.aar`。仓库默认不提交该二进制,
|
||||
> 因此首次构建 Android 之前需要运行 `scripts/build_media3_ffmpeg.sh` 生成产物
|
||||
> (NDK r26b、~30–60 分钟)。AAR 缺失时 plugin 的 `preBuild` 会 fail-fast 抛出
|
||||
> 包含恢复指令的错误。仅构建 Windows / macOS 桌面端时无需此步骤。详见
|
||||
> `plugins/media3_engine/README.md`。
|
||||
|
||||
## Windows Installer (`.exe`)
|
||||
|
||||
Produces `build/installer/smplayer-setup-<version>-x64.exe` via Inno Setup 6.
|
||||
|
||||
**Prerequisite (one-time)** — install Inno Setup:
|
||||
|
||||
```powershell
|
||||
winget install -e --id JRSoftware.InnoSetup
|
||||
```
|
||||
|
||||
**Build:**
|
||||
|
||||
```powershell
|
||||
scripts\dev.ps1 installer # full pipeline (flutter build + ISCC)
|
||||
scripts\dev.ps1 installer -SkipFlutterBuild # reuse existing build/ artifacts
|
||||
```
|
||||
|
||||
Layout:
|
||||
|
||||
```
|
||||
installer/
|
||||
├── sm_emby.iss Inno Setup script (DV/HDR engine DLLs + flutter_assets)
|
||||
└── lang/ChineseSimplified.isl Bundled language file (avoids relying on system ISL)
|
||||
```
|
||||
|
||||
The installer is **unsigned** — first launch triggers Windows SmartScreen ("More info → Run anyway"). Add `SignTool=` to `sm_emby.iss` once a `.pfx` certificate is available.
|
||||
|
||||
## Validation
|
||||
|
||||
```sh
|
||||
flutter analyze
|
||||
```
|
||||
|
||||
## Code generation
|
||||
|
||||
`@freezed` / `@JsonSerializable` / `@RestApi` annotations require regenerating part files after changes:
|
||||
|
||||
```sh
|
||||
dart run build_runner build --delete-conflicting-outputs
|
||||
# or
|
||||
scripts\dev.ps1 gen
|
||||
```
|
||||
|
||||
Generated `*.g.dart` / `*.freezed.dart` files **must be committed** (marked `linguist-generated=true` so GitHub diffs collapse them).
|
||||
@@ -0,0 +1,33 @@
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
linter:
|
||||
rules:
|
||||
prefer_const_constructors: true
|
||||
prefer_const_declarations: true
|
||||
prefer_final_fields: true
|
||||
prefer_final_locals: true
|
||||
avoid_print: true
|
||||
prefer_single_quotes: true
|
||||
sort_child_properties_last: true
|
||||
use_key_in_widget_constructors: true
|
||||
sized_box_for_whitespace: true
|
||||
prefer_is_empty: true
|
||||
prefer_is_not_empty: true
|
||||
unnecessary_lambdas: true
|
||||
always_declare_return_types: true
|
||||
|
||||
analyzer:
|
||||
exclude:
|
||||
# Generated build output can contain partial package snapshots that are not
|
||||
# valid standalone Dart packages and must not participate in app analysis.
|
||||
- build/**
|
||||
# test_link/ is a third-party Flutter example project (media_kit/fvp), not part of sm-emby.
|
||||
- test_link/**
|
||||
errors:
|
||||
invalid_annotation_target: ignore
|
||||
# Emby DTO fields intentionally mirror upstream JSON casing.
|
||||
non_constant_identifier_names: ignore
|
||||
# Current stable Flutter still accepts these APIs; migrate during the next UI pass.
|
||||
deprecated_member_use: ignore
|
||||
# Keep collection conditionals compatible with the existing code style.
|
||||
use_null_aware_elements: ignore
|
||||
@@ -0,0 +1,14 @@
|
||||
gradle-wrapper.jar
|
||||
/.gradle
|
||||
/captures/
|
||||
/gradlew
|
||||
/gradlew.bat
|
||||
/local.properties
|
||||
GeneratedPluginRegistrant.java
|
||||
.cxx/
|
||||
|
||||
# Remember to never publicly share your keystore.
|
||||
# See https://flutter.dev/to/reference-keystore
|
||||
key.properties
|
||||
**/*.keystore
|
||||
**/*.jks
|
||||
@@ -0,0 +1,108 @@
|
||||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("kotlin-android")
|
||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
}
|
||||
|
||||
val localProperties = Properties().apply {
|
||||
val localPropertiesFile = rootProject.file("local.properties")
|
||||
if (localPropertiesFile.exists()) {
|
||||
localPropertiesFile.inputStream().use { load(it) }
|
||||
}
|
||||
}
|
||||
val developmentVersionCode = System.getenv("SMPLAYER_ANDROID_VERSION_CODE")?.let { rawVersionCode ->
|
||||
rawVersionCode.toIntOrNull()?.takeIf { versionCode -> versionCode > 0 }
|
||||
?: error("SMPLAYER_ANDROID_VERSION_CODE must be a positive integer")
|
||||
}
|
||||
val flutterVersionCode = developmentVersionCode
|
||||
?: localProperties.getProperty("flutter.versionCode")?.toIntOrNull()
|
||||
?: 1
|
||||
val flutterVersionName = localProperties.getProperty("flutter.versionName") ?: "1.0.0"
|
||||
|
||||
val keystorePropertiesFile = rootProject.file("key.properties")
|
||||
val keystoreProperties = Properties().apply {
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
keystorePropertiesFile.inputStream().use { load(it) }
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.smplayer.smplayer"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.smplayer.smplayer"
|
||||
// fvp(libmdk) 与 media_kit(libmpv) 在 Android 上的硬解基线。
|
||||
minSdk = maxOf(flutter.minSdkVersion, 24)
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutterVersionCode
|
||||
versionName = flutterVersionName
|
||||
// ABI 限制策略:见下方 buildTypes。defaultConfig 不收缩,保留多 ABI 让
|
||||
// debug 在 x86_64 emulator 上仍能加载 fvp libmdk.so / media_kit libmpv.so;
|
||||
// release 包仅 arm64-v8a 以匹配 media3 FFmpeg AAR 并控制包体。
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
create("release") {
|
||||
storeFile = file(keystoreProperties["storeFile"] as String)
|
||||
storePassword = keystoreProperties["storePassword"] as String
|
||||
keyAlias = keystoreProperties["keyAlias"] as String
|
||||
keyPassword = keystoreProperties["keyPassword"] as String
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 本地 debug 在没有 key.properties 时使用 Android debug 签名;release 构建
|
||||
// 绝不回退到临时 debug 证书,避免发布无法覆盖安装的 APK。
|
||||
val releaseSigningConfig = if (keystorePropertiesFile.exists()) {
|
||||
signingConfigs.getByName("release")
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
applicationIdSuffix = ".dev"
|
||||
manifestPlaceholders["appLabel"] = "smplayer-dev"
|
||||
signingConfig = releaseSigningConfig ?: signingConfigs.getByName("debug")
|
||||
}
|
||||
release {
|
||||
manifestPlaceholders["appLabel"] = "smplayer"
|
||||
if (releaseSigningConfig != null) {
|
||||
signingConfig = releaseSigningConfig
|
||||
}
|
||||
ndk {
|
||||
// 仅 release 收缩到 arm64-v8a:与 media3_engine plugin 自编译的
|
||||
// FFmpeg decoder AAR 对齐(其只含 arm64-v8a),包体最小化。
|
||||
// debug 不限制 → x86_64 / arm64 emulator 上 fvp / media_kit 仍可运行。
|
||||
abiFilters += listOf("arm64-v8a")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
|
||||
// media3_engine plugin 把 media3-decoder-ffmpeg-*.aar 标为 compileOnly(AGP 8
|
||||
// 禁止 library module 直接 implementation 本地 .aar)。在 app 这一层把所有本地
|
||||
// .aar 实际打入 APK;目前只有 media3 FFmpeg decoder AAR,使用 include 通配避免
|
||||
// 后续新增 AAR 时再改这里。
|
||||
dependencies {
|
||||
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar"))))
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
# Media3 FFmpeg decoder AAR 校验清单。
|
||||
#
|
||||
# 格式:<SHA-256> <文件名>
|
||||
#
|
||||
# 由 scripts/build_media3_ffmpeg.sh 产出 AAR 后维护此文件(自动覆写对应文件名条目)。
|
||||
# Gradle 任务 `verifyMedia3FfmpegAar` 在 preBuild 前会读取此清单 + 计算 AAR 实际
|
||||
# SHA-256,不一致即 fail-fast(见 plugins/media3_engine/android/build.gradle.kts)。
|
||||
#
|
||||
# 升级版本时同时更新下方文件名(含 media3 tag)与 SHA-256。
|
||||
|
||||
8e4979b0b9f1408314b149a9740310034238cb80f55167f388f5dbc310e8229e media3-decoder-ffmpeg-1.10.1-arm64.aar
|
||||
Binary file not shown.
@@ -0,0 +1,70 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
|
||||
<application
|
||||
android:label="${appLabel}"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:usesCleartextTraffic="true">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:supportsPictureInPicture="true"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
<!-- Trakt OAuth deep link callback: smplayer://oauth-trakt -->
|
||||
<intent-filter android:autoVerify="false">
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="smplayer" android:host="oauth-trakt"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.EnableImpeller"
|
||||
android:value="true" />
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_provider_paths" />
|
||||
</provider>
|
||||
</application>
|
||||
<!-- Required to query activities that can process text, see:
|
||||
https://developer.android.com/training/package-visibility and
|
||||
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||
|
||||
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
@@ -0,0 +1,109 @@
|
||||
package com.smplayer.smplayer
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Base64
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import java.io.File
|
||||
import java.security.KeyStore
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
|
||||
class AndroidTlsBridge(
|
||||
private val context: Context,
|
||||
flutterEngine: FlutterEngine,
|
||||
) {
|
||||
companion object {
|
||||
private const val CHANNEL_NAME = "smplayer/android_tls"
|
||||
private const val CA_BUNDLE_FILE_NAME = "android-trusted-ca-bundle.pem"
|
||||
}
|
||||
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
private val worker = Executors.newSingleThreadExecutor()
|
||||
private val methodChannel = MethodChannel(
|
||||
flutterEngine.dartExecutor.binaryMessenger,
|
||||
CHANNEL_NAME,
|
||||
)
|
||||
|
||||
@Volatile
|
||||
private var disposed = false
|
||||
|
||||
init {
|
||||
methodChannel.setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"exportTrustedCaBundle" -> worker.execute {
|
||||
try {
|
||||
val bundlePath = exportTrustedCaBundle()
|
||||
postResult { result.success(bundlePath) }
|
||||
} catch (exception: Exception) {
|
||||
postResult {
|
||||
result.error(
|
||||
"CA_EXPORT_FAILED",
|
||||
exception.message ?: "Failed to export Android trusted CAs",
|
||||
null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun exportTrustedCaBundle(): String {
|
||||
val trustStore = KeyStore.getInstance("AndroidCAStore").apply {
|
||||
load(null)
|
||||
}
|
||||
val encodedCertificates = linkedSetOf<String>()
|
||||
val aliases = trustStore.aliases().toList().sorted()
|
||||
|
||||
for (alias in aliases) {
|
||||
val certificate = trustStore.getCertificate(alias) as? X509Certificate
|
||||
?: continue
|
||||
encodedCertificates += Base64.encodeToString(
|
||||
certificate.encoded,
|
||||
Base64.NO_WRAP,
|
||||
)
|
||||
}
|
||||
|
||||
check(encodedCertificates.isNotEmpty()) {
|
||||
"AndroidCAStore contained no X.509 trust anchors"
|
||||
}
|
||||
|
||||
val outputFile = File(context.cacheDir, CA_BUNDLE_FILE_NAME)
|
||||
val temporaryFile = File(context.cacheDir, "$CA_BUNDLE_FILE_NAME.tmp")
|
||||
temporaryFile.bufferedWriter(Charsets.US_ASCII).use { writer ->
|
||||
for (encodedCertificate in encodedCertificates) {
|
||||
writer.appendLine("-----BEGIN CERTIFICATE-----")
|
||||
encodedCertificate.chunked(64).forEach(writer::appendLine)
|
||||
writer.appendLine("-----END CERTIFICATE-----")
|
||||
}
|
||||
}
|
||||
|
||||
if (outputFile.exists() && !outputFile.delete()) {
|
||||
throw IllegalStateException("Could not replace cached CA bundle")
|
||||
}
|
||||
if (!temporaryFile.renameTo(outputFile)) {
|
||||
temporaryFile.copyTo(outputFile, overwrite = true)
|
||||
temporaryFile.delete()
|
||||
}
|
||||
return outputFile.absolutePath
|
||||
}
|
||||
|
||||
private fun postResult(callback: () -> Unit) {
|
||||
if (disposed) return
|
||||
mainHandler.post {
|
||||
if (!disposed) callback()
|
||||
}
|
||||
}
|
||||
|
||||
fun dispose() {
|
||||
disposed = true
|
||||
methodChannel.setMethodCallHandler(null)
|
||||
worker.shutdownNow()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.smplayer.smplayer
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import androidx.core.content.FileProvider
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import java.io.File
|
||||
|
||||
|
||||
class ApkInstallerBridge(
|
||||
private val context: Context,
|
||||
flutterEngine: FlutterEngine,
|
||||
) {
|
||||
companion object {
|
||||
private const val CHANNEL_NAME = "smplayer/apk_installer"
|
||||
}
|
||||
|
||||
private val methodChannel = MethodChannel(
|
||||
flutterEngine.dartExecutor.binaryMessenger, CHANNEL_NAME
|
||||
)
|
||||
|
||||
init {
|
||||
methodChannel.setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"installApk" -> {
|
||||
val filePath = call.argument<String>("path")
|
||||
if (filePath.isNullOrEmpty()) {
|
||||
result.error("INVALID_PATH", "APK file path is null or empty", null)
|
||||
return@setMethodCallHandler
|
||||
}
|
||||
try {
|
||||
launchApkInstall(filePath)
|
||||
result.success(true)
|
||||
} catch (exception: Exception) {
|
||||
result.error(
|
||||
"INSTALL_FAILED",
|
||||
exception.message ?: "Failed to launch APK installer",
|
||||
null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
"canRequestInstall" -> {
|
||||
val canRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.packageManager.canRequestPackageInstalls()
|
||||
} else {
|
||||
true
|
||||
}
|
||||
result.success(canRequest)
|
||||
}
|
||||
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchApkInstall(filePath: String) {
|
||||
val apkFile = File(filePath)
|
||||
if (!apkFile.exists()) {
|
||||
throw IllegalArgumentException("APK file does not exist: $filePath")
|
||||
}
|
||||
|
||||
val authority = "${context.packageName}.fileprovider"
|
||||
val contentUri = FileProvider.getUriForFile(context, authority, apkFile)
|
||||
|
||||
val installIntent = Intent(Intent.ACTION_VIEW).apply {
|
||||
setDataAndType(contentUri, "application/vnd.android.package-archive")
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
|
||||
context.startActivity(installIntent)
|
||||
}
|
||||
|
||||
fun dispose() {
|
||||
methodChannel.setMethodCallHandler(null)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.smplayer.smplayer
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.media.AudioManager
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
|
||||
class MainActivity : FlutterActivity() {
|
||||
private var systemAudio: SystemAudioBridge? = null
|
||||
private var pipBridge: PipBridge? = null
|
||||
private var apkInstaller: ApkInstallerBridge? = null
|
||||
private var androidTls: AndroidTlsBridge? = null
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
systemAudio = SystemAudioBridge(applicationContext, flutterEngine)
|
||||
pipBridge = PipBridge(this, flutterEngine)
|
||||
apkInstaller = ApkInstallerBridge(applicationContext, flutterEngine)
|
||||
androidTls = AndroidTlsBridge(applicationContext, flutterEngine)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
volumeControlStream = AudioManager.STREAM_MUSIC
|
||||
}
|
||||
|
||||
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
||||
val bridge = systemAudio
|
||||
if (bridge != null && bridge.keyInterceptEnabled) {
|
||||
val isVolumeUp = event.keyCode == KeyEvent.KEYCODE_VOLUME_UP
|
||||
val isVolumeDown = event.keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
|
||||
if (isVolumeUp || isVolumeDown) {
|
||||
if (event.action == KeyEvent.ACTION_DOWN) {
|
||||
bridge.adjustVolume(
|
||||
if (isVolumeUp) 1 else -1,
|
||||
showSystemUi = false,
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
return super.dispatchKeyEvent(event)
|
||||
}
|
||||
|
||||
override fun onUserLeaveHint() {
|
||||
super.onUserLeaveHint()
|
||||
pipBridge?.onUserLeaveHint()
|
||||
}
|
||||
|
||||
override fun onPictureInPictureModeChanged(
|
||||
isInPictureInPictureMode: Boolean,
|
||||
newConfig: Configuration,
|
||||
) {
|
||||
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
|
||||
pipBridge?.onPipModeChanged(isInPictureInPictureMode)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
pipBridge?.dispose()
|
||||
pipBridge = null
|
||||
apkInstaller?.dispose()
|
||||
apkInstaller = null
|
||||
androidTls?.dispose()
|
||||
androidTls = null
|
||||
systemAudio?.dispose()
|
||||
systemAudio = null
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
package com.smplayer.smplayer
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.PictureInPictureParams
|
||||
import android.app.RemoteAction
|
||||
import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Rational
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
class PipBridge(
|
||||
private val activity: Activity,
|
||||
flutterEngine: FlutterEngine,
|
||||
) {
|
||||
companion object {
|
||||
private const val METHOD_CHANNEL = "smplayer/pip"
|
||||
private const val EVENT_CHANNEL = "smplayer/pip_events"
|
||||
|
||||
private const val ACTION_PLAY = "com.smplayer.pip.PLAY"
|
||||
private const val ACTION_PAUSE = "com.smplayer.pip.PAUSE"
|
||||
private const val ACTION_NEXT = "com.smplayer.pip.NEXT"
|
||||
private const val ACTION_PREV = "com.smplayer.pip.PREV"
|
||||
private const val ACTION_SEEK_FWD = "com.smplayer.pip.SEEK_FWD"
|
||||
private const val ACTION_SEEK_BWD = "com.smplayer.pip.SEEK_BWD"
|
||||
|
||||
private const val REQUEST_PLAY = 1
|
||||
private const val REQUEST_PAUSE = 2
|
||||
private const val REQUEST_NEXT = 3
|
||||
private const val REQUEST_PREV = 4
|
||||
private const val REQUEST_SEEK_FWD = 5
|
||||
private const val REQUEST_SEEK_BWD = 6
|
||||
}
|
||||
|
||||
private val methodChannel = MethodChannel(
|
||||
flutterEngine.dartExecutor.binaryMessenger, METHOD_CHANNEL
|
||||
)
|
||||
private val eventChannel = EventChannel(
|
||||
flutterEngine.dartExecutor.binaryMessenger, EVENT_CHANNEL
|
||||
)
|
||||
|
||||
private var eventSink: EventChannel.EventSink? = null
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
private var autoEnterOnLeave = false
|
||||
private var aspectX = 16
|
||||
private var aspectY = 9
|
||||
private var isPlaying = false
|
||||
private var hasNext = false
|
||||
private var hasPrev = false
|
||||
private var useEpisodeActions = true
|
||||
|
||||
private val actionReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
val action = intent?.action ?: return
|
||||
val dartAction = when (action) {
|
||||
ACTION_PLAY -> "play"
|
||||
ACTION_PAUSE -> "pause"
|
||||
ACTION_NEXT -> "next"
|
||||
ACTION_PREV -> "prev"
|
||||
ACTION_SEEK_FWD -> "seekForward"
|
||||
ACTION_SEEK_BWD -> "seekBackward"
|
||||
else -> return
|
||||
}
|
||||
mainHandler.post {
|
||||
eventSink?.success(mapOf("type" to "action", "action" to dartAction))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var receiverRegistered = false
|
||||
|
||||
init {
|
||||
methodChannel.setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"isSupported" -> {
|
||||
result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
}
|
||||
|
||||
"isInPipMode" -> {
|
||||
result.success(
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
activity.isInPictureInPictureMode
|
||||
else false
|
||||
)
|
||||
}
|
||||
|
||||
"configure" -> {
|
||||
val args = call.arguments as? Map<*, *>
|
||||
if (args != null) {
|
||||
aspectX = (args["aspectX"] as? Number)?.toInt() ?: 16
|
||||
aspectY = (args["aspectY"] as? Number)?.toInt() ?: 9
|
||||
autoEnterOnLeave = args["autoEnterOnLeave"] as? Boolean ?: false
|
||||
isPlaying = args["isPlaying"] as? Boolean ?: false
|
||||
hasNext = args["hasNext"] as? Boolean ?: false
|
||||
hasPrev = args["hasPrev"] as? Boolean ?: false
|
||||
useEpisodeActions = args["useEpisodeActions"] as? Boolean ?: true
|
||||
updatePipParams()
|
||||
}
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
"enterPip" -> {
|
||||
val args = call.arguments as? Map<*, *>
|
||||
if (args != null) {
|
||||
aspectX = (args["aspectX"] as? Number)?.toInt() ?: aspectX
|
||||
aspectY = (args["aspectY"] as? Number)?.toInt() ?: aspectY
|
||||
}
|
||||
result.success(enterPip())
|
||||
}
|
||||
|
||||
"updatePlaybackState" -> {
|
||||
val args = call.arguments as? Map<*, *>
|
||||
if (args != null) {
|
||||
isPlaying = args["isPlaying"] as? Boolean ?: isPlaying
|
||||
hasNext = args["hasNext"] as? Boolean ?: hasNext
|
||||
hasPrev = args["hasPrev"] as? Boolean ?: hasPrev
|
||||
useEpisodeActions = args["useEpisodeActions"] as? Boolean ?: useEpisodeActions
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
|
||||
activity.isInPictureInPictureMode
|
||||
) {
|
||||
updatePipParams()
|
||||
}
|
||||
}
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
eventChannel.setStreamHandler(object : EventChannel.StreamHandler {
|
||||
override fun onListen(arguments: Any?, sink: EventChannel.EventSink?) {
|
||||
eventSink = sink
|
||||
registerActionReceiver()
|
||||
}
|
||||
|
||||
override fun onCancel(arguments: Any?) {
|
||||
eventSink = null
|
||||
unregisterActionReceiver()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun registerActionReceiver() {
|
||||
if (receiverRegistered) return
|
||||
val filter = IntentFilter().apply {
|
||||
addAction(ACTION_PLAY)
|
||||
addAction(ACTION_PAUSE)
|
||||
addAction(ACTION_NEXT)
|
||||
addAction(ACTION_PREV)
|
||||
addAction(ACTION_SEEK_FWD)
|
||||
addAction(ACTION_SEEK_BWD)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
activity.registerReceiver(actionReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
|
||||
} else {
|
||||
activity.registerReceiver(actionReceiver, filter)
|
||||
}
|
||||
receiverRegistered = true
|
||||
}
|
||||
|
||||
private fun unregisterActionReceiver() {
|
||||
if (!receiverRegistered) return
|
||||
try {
|
||||
activity.unregisterReceiver(actionReceiver)
|
||||
} catch (_: IllegalArgumentException) {}
|
||||
receiverRegistered = false
|
||||
}
|
||||
|
||||
private fun buildRemoteActions(): List<RemoteAction> {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return emptyList()
|
||||
|
||||
val actions = mutableListOf<RemoteAction>()
|
||||
|
||||
if (useEpisodeActions && (hasNext || hasPrev)) {
|
||||
|
||||
actions.add(
|
||||
makeAction(
|
||||
android.R.drawable.ic_media_previous,
|
||||
"上一集",
|
||||
ACTION_PREV,
|
||||
REQUEST_PREV,
|
||||
enabled = hasPrev,
|
||||
)
|
||||
)
|
||||
actions.add(
|
||||
if (isPlaying) makeAction(
|
||||
android.R.drawable.ic_media_pause,
|
||||
"暂停",
|
||||
ACTION_PAUSE,
|
||||
REQUEST_PAUSE,
|
||||
) else makeAction(
|
||||
android.R.drawable.ic_media_play,
|
||||
"播放",
|
||||
ACTION_PLAY,
|
||||
REQUEST_PLAY,
|
||||
)
|
||||
)
|
||||
actions.add(
|
||||
makeAction(
|
||||
android.R.drawable.ic_media_next,
|
||||
"下一集",
|
||||
ACTION_NEXT,
|
||||
REQUEST_NEXT,
|
||||
enabled = hasNext,
|
||||
)
|
||||
)
|
||||
} else {
|
||||
|
||||
actions.add(
|
||||
makeAction(
|
||||
android.R.drawable.ic_media_rew,
|
||||
"快退",
|
||||
ACTION_SEEK_BWD,
|
||||
REQUEST_SEEK_BWD,
|
||||
)
|
||||
)
|
||||
actions.add(
|
||||
if (isPlaying) makeAction(
|
||||
android.R.drawable.ic_media_pause,
|
||||
"暂停",
|
||||
ACTION_PAUSE,
|
||||
REQUEST_PAUSE,
|
||||
) else makeAction(
|
||||
android.R.drawable.ic_media_play,
|
||||
"播放",
|
||||
ACTION_PLAY,
|
||||
REQUEST_PLAY,
|
||||
)
|
||||
)
|
||||
actions.add(
|
||||
makeAction(
|
||||
android.R.drawable.ic_media_ff,
|
||||
"快进",
|
||||
ACTION_SEEK_FWD,
|
||||
REQUEST_SEEK_FWD,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return actions
|
||||
}
|
||||
|
||||
private fun makeAction(
|
||||
iconRes: Int,
|
||||
title: String,
|
||||
action: String,
|
||||
requestCode: Int,
|
||||
enabled: Boolean = true,
|
||||
): RemoteAction {
|
||||
val intent = Intent(action).setPackage(activity.packageName)
|
||||
val flags = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
val pendingIntent = PendingIntent.getBroadcast(activity, requestCode, intent, flags)
|
||||
val icon = Icon.createWithResource(activity, iconRes)
|
||||
return RemoteAction(icon, title, title, pendingIntent).apply {
|
||||
isEnabled = enabled
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildPipParams(): PictureInPictureParams? {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return null
|
||||
|
||||
val builder = PictureInPictureParams.Builder()
|
||||
.setAspectRatio(Rational(aspectX, aspectY))
|
||||
.setActions(buildRemoteActions())
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
builder.setAutoEnterEnabled(autoEnterOnLeave && isPlaying)
|
||||
builder.setSeamlessResizeEnabled(true)
|
||||
}
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
private fun updatePipParams() {
|
||||
val params = buildPipParams() ?: return
|
||||
try {
|
||||
activity.setPictureInPictureParams(params)
|
||||
} catch (e: Exception) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
fun enterPip(): Boolean {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return false
|
||||
val params = buildPipParams() ?: return false
|
||||
return try {
|
||||
activity.enterPictureInPictureMode(params)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun onUserLeaveHint() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) return
|
||||
if (!autoEnterOnLeave || !isPlaying) return
|
||||
enterPip()
|
||||
}
|
||||
|
||||
|
||||
fun onPipModeChanged(isInPip: Boolean) {
|
||||
mainHandler.post {
|
||||
eventSink?.success(mapOf("type" to "modeChanged", "isInPip" to isInPip))
|
||||
}
|
||||
}
|
||||
|
||||
fun dispose() {
|
||||
unregisterActionReceiver()
|
||||
methodChannel.setMethodCallHandler(null)
|
||||
eventChannel.setStreamHandler(null)
|
||||
eventSink = null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package com.smplayer.smplayer
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.media.AudioManager
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
class SystemAudioBridge(
|
||||
private val context: Context,
|
||||
flutterEngine: FlutterEngine,
|
||||
) {
|
||||
companion object {
|
||||
private const val METHOD_CHANNEL = "smplayer/system_audio"
|
||||
private const val EVENT_CHANNEL = "smplayer/system_audio_events"
|
||||
private const val VOLUME_CHANGED_ACTION = "android.media.VOLUME_CHANGED_ACTION"
|
||||
private const val STREAM_TYPE = AudioManager.STREAM_MUSIC
|
||||
}
|
||||
|
||||
private val audioManager: AudioManager =
|
||||
context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
|
||||
private val methodChannel = MethodChannel(
|
||||
flutterEngine.dartExecutor.binaryMessenger, METHOD_CHANNEL
|
||||
)
|
||||
private val eventChannel = EventChannel(
|
||||
flutterEngine.dartExecutor.binaryMessenger, EVENT_CHANNEL
|
||||
)
|
||||
|
||||
private var eventSink: EventChannel.EventSink? = null
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
@Volatile
|
||||
var keyInterceptEnabled: Boolean = false
|
||||
private set
|
||||
|
||||
private val volumeReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(ctx: Context?, intent: Intent?) {
|
||||
if (intent?.action == VOLUME_CHANGED_ACTION) {
|
||||
val streamType = intent.getIntExtra(
|
||||
"android.media.EXTRA_VOLUME_STREAM_TYPE", -1
|
||||
)
|
||||
if (streamType == STREAM_TYPE) {
|
||||
postVolumeEvent()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
methodChannel.setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"getVolume" -> result.success(currentVolumeMap())
|
||||
"adjustVolume" -> {
|
||||
val delta = (call.argument<Int>("delta") ?: 0)
|
||||
adjustVolume(delta, showSystemUi = false)
|
||||
result.success(currentVolumeMap())
|
||||
}
|
||||
"setKeyIntercept" -> {
|
||||
keyInterceptEnabled = call.argument<Boolean>("enabled") ?: false
|
||||
result.success(null)
|
||||
}
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
eventChannel.setStreamHandler(object : EventChannel.StreamHandler {
|
||||
override fun onListen(arguments: Any?, sink: EventChannel.EventSink?) {
|
||||
eventSink = sink
|
||||
context.registerReceiver(
|
||||
volumeReceiver, IntentFilter(VOLUME_CHANGED_ACTION)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCancel(arguments: Any?) {
|
||||
try {
|
||||
context.unregisterReceiver(volumeReceiver)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
}
|
||||
eventSink = null
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun adjustVolume(delta: Int, showSystemUi: Boolean) {
|
||||
val direction = when {
|
||||
delta > 0 -> AudioManager.ADJUST_RAISE
|
||||
delta < 0 -> AudioManager.ADJUST_LOWER
|
||||
else -> return
|
||||
}
|
||||
val flag = if (showSystemUi) AudioManager.FLAG_SHOW_UI else 0
|
||||
repeat(kotlin.math.abs(delta).coerceAtMost(20)) {
|
||||
audioManager.adjustStreamVolume(STREAM_TYPE, direction, flag)
|
||||
}
|
||||
postVolumeEvent()
|
||||
}
|
||||
|
||||
private fun currentVolumeMap(): Map<String, Int> = mapOf(
|
||||
"current" to audioManager.getStreamVolume(STREAM_TYPE),
|
||||
"max" to audioManager.getStreamMaxVolume(STREAM_TYPE),
|
||||
)
|
||||
|
||||
private fun postVolumeEvent() {
|
||||
val payload = currentVolumeMap()
|
||||
mainHandler.post {
|
||||
eventSink?.success(payload)
|
||||
}
|
||||
}
|
||||
|
||||
fun dispose() {
|
||||
try {
|
||||
context.unregisterReceiver(volumeReceiver)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
}
|
||||
methodChannel.setMethodCallHandler(null)
|
||||
eventChannel.setStreamHandler(null)
|
||||
eventSink = null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="?android:colorBackground" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@android:color/white" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<!-- UpdateService 下载 APK 至 getTemporaryDirectory() 下的 smplayer-update/ -->
|
||||
<cache-path name="update_apk" path="smplayer-update/" />
|
||||
</paths>
|
||||
@@ -0,0 +1,24 @@
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
val newBuildDir: Directory =
|
||||
rootProject.layout.buildDirectory
|
||||
.dir("../../build")
|
||||
.get()
|
||||
rootProject.layout.buildDirectory.value(newBuildDir)
|
||||
|
||||
subprojects {
|
||||
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
|
||||
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
||||
}
|
||||
subprojects {
|
||||
project.evaluationDependsOn(":app")
|
||||
}
|
||||
|
||||
tasks.register<Delete>("clean") {
|
||||
delete(rootProject.layout.buildDirectory)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=512m -XX:ReservedCodeCacheSize=256m
|
||||
# 让 Gradle 直连、不读 macOS 系统代理,避免 Reqable/Charles 等抓包代理残留导致依赖下载挂起或连接被拒
|
||||
systemProp.java.net.useSystemProxies=false
|
||||
android.useAndroidX=true
|
||||
# This builtInKotlin flag was added automatically by Flutter migrator
|
||||
android.builtInKotlin=false
|
||||
# This newDsl flag was added automatically by Flutter migrator
|
||||
android.newDsl=false
|
||||
@@ -0,0 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
|
||||
@@ -0,0 +1,14 @@
|
||||
# 本地 release 签名配置。
|
||||
# 复制为 key.properties 并填入你的 keystore 信息。
|
||||
# key.properties 已被 .gitignore 忽略,不会提交到仓库。
|
||||
#
|
||||
# 生成 keystore:
|
||||
# keytool -genkey -v -keystore release.keystore -alias smplayer \
|
||||
# -keyalg RSA -keysize 2048 -validity 10000
|
||||
#
|
||||
# storeFile 路径相对于 android/app/ 目录解析。
|
||||
|
||||
storeFile=release.keystore
|
||||
storePassword=
|
||||
keyAlias=smplayer
|
||||
keyPassword=
|
||||
@@ -0,0 +1,26 @@
|
||||
pluginManagement {
|
||||
val flutterSdkPath =
|
||||
run {
|
||||
val properties = java.util.Properties()
|
||||
file("local.properties").inputStream().use { properties.load(it) }
|
||||
val flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
|
||||
flutterSdkPath
|
||||
}
|
||||
|
||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||
id("com.android.application") version "8.11.1" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
|
||||
}
|
||||
|
||||
include(":app")
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 4.2 MiB |
@@ -0,0 +1,29 @@
|
||||
# assets/js
|
||||
|
||||
## cheerio.min.js
|
||||
|
||||
打包好的 [cheerio](https://github.com/cheeriojs/cheerio)(含 parse5 解析器),注入到
|
||||
fjs(QuickJS)脚本组件运行环境,作为 `Widget.html.load` / `Widget.dom.*` 的实现。
|
||||
上游 ForwardWidgets 宿主的约定就是 "Widget has built-in cheerio",脚本以同步
|
||||
cheerio API 消费 DOM——fjs 的 Dart 桥(`fjs.bridge_call`)只有异步 Promise 形态,
|
||||
因此 DOM 解析必须完整运行在 JS 侧,不能走宿主桥。
|
||||
|
||||
全局导出名:`__fjsCheerio`(`__fjsCheerio.load(html)`)。bundle 头部带一个 `atob`
|
||||
polyfill(`entities` 包解码实体表需要,QuickJS 没有内置 atob/Buffer)。
|
||||
|
||||
### 重新构建
|
||||
|
||||
```bash
|
||||
mkdir /tmp/cheerio-bundle && cd /tmp/cheerio-bundle
|
||||
npm install cheerio@1.1.2 esbuild
|
||||
echo "export { load } from 'cheerio';" > entry.js
|
||||
npx esbuild entry.js --bundle --format=iife --global-name=__fjsCheerio \
|
||||
--platform=browser --minify --banner:js="$(cat banner.js)" \
|
||||
--outfile=cheerio.min.js
|
||||
```
|
||||
|
||||
`banner.js` 为 atob polyfill,内容见当前 bundle 文件头部(第 1 段
|
||||
`if (typeof globalThis.atob === 'undefined')`)。`--platform=browser` 会选择
|
||||
cheerio 的 browser 入口(只含 load/parse,不含依赖 Buffer/undici 的
|
||||
loadBuffer/fromURL)。构建后用一个无 Node 全局的裸 `vm` 沙箱冒烟验证
|
||||
`load/text/attr/closest/Array.from` 再提交。
|
||||
Vendored
+20
File diff suppressed because one or more lines are too long
@@ -0,0 +1,5 @@
|
||||
# fjs(脚本组件 JS 引擎)的原生 Rust 库 libfjs 经 cargokit 构建。
|
||||
# 本机装有 rustup 时 cargokit 默认从源码编译——但 libfjs 的依赖链要求较新的
|
||||
# rustc(1.88 已不满足)。这里显式改用 fjs 官方 GitHub Release 的签名预编译
|
||||
# 产物(公钥见 fjs 包内 libfjs/cargokit.yaml),构建应用无需 Rust 工具链。
|
||||
use_precompiled_binaries: true
|
||||
@@ -0,0 +1,418 @@
|
||||
; *** Inno Setup version 6.5.0+ Chinese Simplified messages ***
|
||||
;
|
||||
; To download user-contributed translations of this file, go to:
|
||||
; https://jrsoftware.org/files/istrans/
|
||||
;
|
||||
; Note: When translating this text, do not add periods (.) to the end of
|
||||
; messages that didn't have them already, because on those messages Inno
|
||||
; Setup adds the periods automatically (appending a period would result in
|
||||
; two periods being displayed).
|
||||
;
|
||||
; Maintained by Zhenghan Yang
|
||||
; Email: 847320916@QQ.com
|
||||
; Translation based on network resource
|
||||
; The latest Translation is on https://github.com/kira-96/Inno-Setup-Chinese-Simplified-Translation
|
||||
;
|
||||
|
||||
[LangOptions]
|
||||
; The following three entries are very important. Be sure to read and
|
||||
; understand the '[LangOptions] section' topic in the help file.
|
||||
LanguageName=简体中文
|
||||
; If Language Name display incorrect, uncomment next line
|
||||
; LanguageName=<7B80><4F53><4E2D><6587>
|
||||
; About LanguageID, to reference link:
|
||||
; https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/a9eac961-e77d-41a6-90a5-ce1a8b0cdb9c
|
||||
LanguageID=$0804
|
||||
; About CodePage, to reference link:
|
||||
; https://docs.microsoft.com/en-us/windows/win32/intl/code-page-identifiers
|
||||
LanguageCodePage=936
|
||||
; If the language you are translating to requires special font faces or
|
||||
; sizes, uncomment any of the following entries and change them accordingly.
|
||||
;DialogFontName=
|
||||
;DialogFontSize=9
|
||||
;DialogFontBaseScaleWidth=7
|
||||
;DialogFontBaseScaleHeight=15
|
||||
;WelcomeFontName=Segoe UI
|
||||
;WelcomeFontSize=14
|
||||
|
||||
[Messages]
|
||||
|
||||
; *** 应用程序标题
|
||||
SetupAppTitle=安装
|
||||
SetupWindowTitle=安装 - %1
|
||||
UninstallAppTitle=卸载
|
||||
UninstallAppFullTitle=%1 卸载
|
||||
|
||||
; *** Misc. common
|
||||
InformationTitle=信息
|
||||
ConfirmTitle=确认
|
||||
ErrorTitle=错误
|
||||
|
||||
; *** SetupLdr messages
|
||||
SetupLdrStartupMessage=现在将安装 %1。您想要继续吗?
|
||||
LdrCannotCreateTemp=无法创建临时文件。安装程序已中止
|
||||
LdrCannotExecTemp=无法执行临时目录中的文件。安装程序已中止
|
||||
HelpTextNote=
|
||||
|
||||
; *** 启动错误消息
|
||||
LastErrorMessage=%1。%n%n错误 %2: %3
|
||||
SetupFileMissing=安装目录中缺少文件 %1。请修正这个问题或者获取程序的新副本。
|
||||
SetupFileCorrupt=安装文件已损坏。请获取程序的新副本。
|
||||
SetupFileCorruptOrWrongVer=安装文件已损坏,或是与这个安装程序的版本不兼容。请修正这个问题或获取新的程序副本。
|
||||
InvalidParameter=无效的命令行参数:%n%n%1
|
||||
SetupAlreadyRunning=安装程序正在运行。
|
||||
WindowsVersionNotSupported=此程序不支持当前计算机运行的 Windows 版本。
|
||||
WindowsServicePackRequired=此程序需要 %1 服务包 %2 或更高版本。
|
||||
NotOnThisPlatform=此程序不能在 %1 上运行。
|
||||
OnlyOnThisPlatform=此程序只能在 %1 上运行。
|
||||
OnlyOnTheseArchitectures=此程序只能安装到为下列处理器架构设计的 Windows 版本中:%n%n%1
|
||||
WinVersionTooLowError=此程序需要 %1 版本 %2 或更高。
|
||||
WinVersionTooHighError=此程序不能安装于 %1 版本 %2 或更高。
|
||||
AdminPrivilegesRequired=在安装此程序时您必须以管理员身份登录。
|
||||
PowerUserPrivilegesRequired=在安装此程序时您必须以管理员身份或有权限的用户组身份登录。
|
||||
SetupAppRunningError=安装程序发现 %1 当前正在运行。%n%n请先关闭正在运行的程序,然后点击“确定”继续,或点击“取消”退出。
|
||||
UninstallAppRunningError=卸载程序发现 %1 当前正在运行。%n%n请先关闭正在运行的程序,然后点击“确定”继续,或点击“取消”退出。
|
||||
|
||||
; *** 启动问题
|
||||
PrivilegesRequiredOverrideTitle=选择安装程序模式
|
||||
PrivilegesRequiredOverrideInstruction=选择安装模式
|
||||
PrivilegesRequiredOverrideText1=%1 可以为所有用户安装(需要管理员权限),或仅为您安装。
|
||||
PrivilegesRequiredOverrideText2=%1 可以仅为您安装,或为所有用户安装(需要管理员权限)。
|
||||
PrivilegesRequiredOverrideAllUsers=为所有用户安装(&A)
|
||||
PrivilegesRequiredOverrideAllUsersRecommended=为所有用户安装(&A) (建议选项)
|
||||
PrivilegesRequiredOverrideCurrentUser=仅为我安装(&M)
|
||||
PrivilegesRequiredOverrideCurrentUserRecommended=仅为我安装(&M) (建议选项)
|
||||
|
||||
; *** 其他错误
|
||||
ErrorCreatingDir=安装程序无法创建目录“%1”
|
||||
ErrorTooManyFilesInDir=无法在目录“%1”中创建文件,因为里面包含太多文件
|
||||
|
||||
; *** 安装程序公共消息
|
||||
ExitSetupTitle=退出安装程序
|
||||
ExitSetupMessage=安装程序尚未完成。如果现在退出,将不会安装该程序。%n%n您之后可以再次运行安装程序完成安装。%n%n现在退出安装程序吗?
|
||||
AboutSetupMenuItem=关于安装程序(&A)...
|
||||
AboutSetupTitle=关于安装程序
|
||||
AboutSetupMessage=%1 版本 %2%n%3%n%n%1 主页:%n%4
|
||||
AboutSetupNote=
|
||||
TranslatorNote=简体中文翻译由Kira(847320916@qq.com)维护。项目地址:https://github.com/kira-96/Inno-Setup-Chinese-Simplified-Translation
|
||||
|
||||
; *** 按钮
|
||||
ButtonBack=< 上一步(&B)
|
||||
ButtonNext=下一步(&N) >
|
||||
ButtonInstall=安装(&I)
|
||||
ButtonOK=确定
|
||||
ButtonCancel=取消
|
||||
ButtonYes=是(&Y)
|
||||
ButtonYesToAll=全是(&A)
|
||||
ButtonNo=否(&N)
|
||||
ButtonNoToAll=全否(&O)
|
||||
ButtonFinish=完成(&F)
|
||||
ButtonBrowse=浏览(&B)...
|
||||
ButtonWizardBrowse=浏览(&R)...
|
||||
ButtonNewFolder=新建文件夹(&M)
|
||||
|
||||
; *** “选择语言”对话框消息
|
||||
SelectLanguageTitle=选择安装语言
|
||||
SelectLanguageLabel=选择安装时使用的语言。
|
||||
|
||||
; *** 公共向导文字
|
||||
ClickNext=点击“下一步”继续,或点击“取消”退出安装程序。
|
||||
BeveledLabel=
|
||||
BrowseDialogTitle=浏览文件夹
|
||||
BrowseDialogLabel=在下面的列表中选择一个文件夹,然后点击“确定”。
|
||||
NewFolderName=新建文件夹
|
||||
|
||||
; *** “欢迎”向导页
|
||||
WelcomeLabel1=欢迎使用 [name] 安装向导
|
||||
WelcomeLabel2=现在将安装 [name/ver] 到您的电脑中。%n%n建议您在继续安装前关闭所有其他应用程序。
|
||||
|
||||
; *** “密码”向导页
|
||||
WizardPassword=密码
|
||||
PasswordLabel1=这个安装程序有密码保护。
|
||||
PasswordLabel3=请输入密码,然后点击“下一步”继续。密码区分大小写。
|
||||
PasswordEditLabel=密码(&P):
|
||||
IncorrectPassword=您输入的密码不正确,请重新输入。
|
||||
|
||||
; *** “许可协议”向导页
|
||||
WizardLicense=许可协议
|
||||
LicenseLabel=请在继续安装前阅读以下重要信息。
|
||||
LicenseLabel3=请仔细阅读下列许可协议。在继续安装前您必须同意这些协议条款。
|
||||
LicenseAccepted=我同意此协议(&A)
|
||||
LicenseNotAccepted=我不同意此协议(&D)
|
||||
|
||||
; *** “信息”向导页
|
||||
WizardInfoBefore=信息
|
||||
InfoBeforeLabel=请在继续安装前阅读以下重要信息。
|
||||
InfoBeforeClickLabel=准备好继续安装后,点击“下一步”。
|
||||
WizardInfoAfter=信息
|
||||
InfoAfterLabel=请在继续安装前阅读以下重要信息。
|
||||
InfoAfterClickLabel=准备好继续安装后,点击“下一步”。
|
||||
|
||||
; *** “用户信息”向导页
|
||||
WizardUserInfo=用户信息
|
||||
UserInfoDesc=请输入您的信息。
|
||||
UserInfoName=用户名(&U):
|
||||
UserInfoOrg=组织(&O):
|
||||
UserInfoSerial=序列号(&S):
|
||||
UserInfoNameRequired=您必须输入用户名。
|
||||
|
||||
; *** “选择目标目录”向导页
|
||||
WizardSelectDir=选择目标位置
|
||||
SelectDirDesc=您想将 [name] 安装在哪里?
|
||||
SelectDirLabel3=安装程序将安装 [name] 到下面的文件夹中。
|
||||
SelectDirBrowseLabel=点击“下一步”继续。如果您想选择其他文件夹,点击“浏览”。
|
||||
DiskSpaceGBLabel=至少需要有 [gb] GB 的可用磁盘空间。
|
||||
DiskSpaceMBLabel=至少需要有 [mb] MB 的可用磁盘空间。
|
||||
CannotInstallToNetworkDrive=安装程序无法安装到一个网络驱动器。
|
||||
CannotInstallToUNCPath=安装程序无法安装到一个 UNC 路径。
|
||||
InvalidPath=您必须输入一个带驱动器卷标的完整路径,例如:%n%nC:\APP%n%n或UNC路径:%n%n\\server\share
|
||||
InvalidDrive=您选定的驱动器或 UNC 共享不存在或不能访问。请选择其他位置。
|
||||
DiskSpaceWarningTitle=磁盘空间不足
|
||||
DiskSpaceWarning=安装程序至少需要 %1 KB 的可用空间才能安装,但选定驱动器只有 %2 KB 的可用空间。%n%n您一定要继续吗?
|
||||
DirNameTooLong=文件夹名称或路径太长。
|
||||
InvalidDirName=文件夹名称无效。
|
||||
BadDirName32=文件夹名称不能包含下列任何字符:%n%n%1
|
||||
DirExistsTitle=文件夹已存在
|
||||
DirExists=文件夹:%n%n%1%n%n已经存在。您一定要安装到这个文件夹中吗?
|
||||
DirDoesntExistTitle=文件夹不存在
|
||||
DirDoesntExist=文件夹:%n%n%1%n%n不存在。您想要创建此文件夹吗?
|
||||
|
||||
; *** “选择组件”向导页
|
||||
WizardSelectComponents=选择组件
|
||||
SelectComponentsDesc=您想安装哪些程序组件?
|
||||
SelectComponentsLabel2=选中您想安装的组件;取消您不想安装的组件。然后点击“下一步”继续。
|
||||
FullInstallation=完全安装
|
||||
; if possible don't translate 'Compact' as 'Minimal' (I mean 'Minimal' in your language)
|
||||
CompactInstallation=简洁安装
|
||||
CustomInstallation=自定义安装
|
||||
NoUninstallWarningTitle=组件已存在
|
||||
NoUninstallWarning=安装程序检测到下列组件已安装在您的电脑中:%n%n%1%n%n取消选中这些组件不会卸载它们。%n%n确定要继续吗?
|
||||
ComponentSize1=%1 KB
|
||||
ComponentSize2=%1 MB
|
||||
ComponentsDiskSpaceGBLabel=当前选择的组件需要至少 [gb] GB 的磁盘空间。
|
||||
ComponentsDiskSpaceMBLabel=当前选择的组件需要至少 [mb] MB 的磁盘空间。
|
||||
|
||||
; *** “选择附加任务”向导页
|
||||
WizardSelectTasks=选择附加任务
|
||||
SelectTasksDesc=您想要安装程序执行哪些附加任务?
|
||||
SelectTasksLabel2=选择您想要安装程序在安装 [name] 时执行的附加任务,然后点击“下一步”。
|
||||
|
||||
; *** “选择开始菜单文件夹”向导页
|
||||
WizardSelectProgramGroup=选择开始菜单文件夹
|
||||
SelectStartMenuFolderDesc=安装程序应该在哪里放置程序的快捷方式?
|
||||
SelectStartMenuFolderLabel3=安装程序将在下列“开始”菜单文件夹中创建程序的快捷方式。
|
||||
SelectStartMenuFolderBrowseLabel=点击“下一步”继续。如果您想选择其他文件夹,点击“浏览”。
|
||||
MustEnterGroupName=您必须输入一个文件夹名。
|
||||
GroupNameTooLong=文件夹名或路径太长。
|
||||
InvalidGroupName=无效的文件夹名字。
|
||||
BadGroupName=文件夹名不能包含下列任何字符:%n%n%1
|
||||
NoProgramGroupCheck2=不创建开始菜单文件夹(&D)
|
||||
|
||||
; *** “准备安装”向导页
|
||||
WizardReady=准备安装
|
||||
ReadyLabel1=安装程序准备就绪,现在可以开始安装 [name] 到您的电脑。
|
||||
ReadyLabel2a=点击“安装”继续此安装程序。如果您想重新考虑或修改任何设置,点击“上一步”。
|
||||
ReadyLabel2b=点击“安装”继续此安装程序。
|
||||
ReadyMemoUserInfo=用户信息:
|
||||
ReadyMemoDir=目标位置:
|
||||
ReadyMemoType=安装类型:
|
||||
ReadyMemoComponents=已选择组件:
|
||||
ReadyMemoGroup=开始菜单文件夹:
|
||||
ReadyMemoTasks=附加任务:
|
||||
|
||||
; *** TExtractionWizardPage 向导页面与 ExtractArchive
|
||||
ExtractingLabel=正在解压文件...
|
||||
ButtonStopExtraction=停止解压(&S)
|
||||
StopExtraction=您确定要停止解压吗?
|
||||
ErrorExtractionAborted=解压已中止
|
||||
ErrorExtractionFailed=解压失败:%1
|
||||
|
||||
; *** 压缩文件解压失败详情
|
||||
ArchiveIncorrectPassword=压缩文件密码不正确
|
||||
ArchiveIsCorrupted=压缩文件已损坏
|
||||
ArchiveUnsupportedFormat=不支持的压缩文件格式
|
||||
|
||||
; *** TDownloadWizardPage 向导页面和 DownloadTemporaryFile
|
||||
DownloadingLabel2=正在下载文件...
|
||||
ButtonStopDownload=停止下载(&S)
|
||||
StopDownload=您确定要停止下载吗?
|
||||
ErrorDownloadAborted=下载已中止
|
||||
ErrorDownloadFailed=下载失败:%1 %2
|
||||
ErrorDownloadSizeFailed=获取下载大小失败:%1 %2
|
||||
ErrorProgress=无效的进度:%1 / %2
|
||||
ErrorFileSize=文件大小错误:预期 %1,实际 %2
|
||||
|
||||
; *** “正在准备安装”向导页
|
||||
WizardPreparing=正在准备安装
|
||||
PreparingDesc=安装程序正在准备安装 [name] 到您的电脑。
|
||||
PreviousInstallNotCompleted=先前的程序安装或卸载未完成,您需要重启您的电脑以完成。%n%n在重启电脑后,再次运行安装程序以完成 [name] 的安装。
|
||||
CannotContinue=安装程序不能继续。请点击“取消”退出。
|
||||
ApplicationsFound=以下应用程序正在使用将由安装程序更新的文件。建议您允许安装程序自动关闭这些应用程序。
|
||||
ApplicationsFound2=以下应用程序正在使用将由安装程序更新的文件。建议您允许安装程序自动关闭这些应用程序。安装完成后,安装程序将尝试重新启动这些应用程序。
|
||||
CloseApplications=自动关闭应用程序(&A)
|
||||
DontCloseApplications=不要关闭应用程序(&D)
|
||||
ErrorCloseApplications=安装程序无法自动关闭所有应用程序。建议您在继续之前,关闭所有在使用需要由安装程序更新的文件的应用程序。
|
||||
PrepareToInstallNeedsRestart=安装程序必须重启您的计算机。计算机重启后,请再次运行安装程序以完成 [name] 的安装。%n%n是否立即重新启动?
|
||||
|
||||
; *** “正在安装”向导页
|
||||
WizardInstalling=正在安装
|
||||
InstallingLabel=安装程序正在安装 [name] 到您的电脑,请稍候。
|
||||
|
||||
; *** “安装完成”向导页
|
||||
FinishedHeadingLabel=[name] 安装完成
|
||||
FinishedLabelNoIcons=安装程序已在您的电脑中安装了 [name]。
|
||||
FinishedLabel=安装程序已在您的电脑中安装了 [name]。您可以通过已安装的快捷方式运行此应用程序。
|
||||
ClickFinish=点击“完成”退出安装程序。
|
||||
FinishedRestartLabel=为完成 [name] 的安装,安装程序必须重新启动您的电脑。要立即重启吗?
|
||||
FinishedRestartMessage=为完成 [name] 的安装,安装程序必须重新启动您的电脑。%n%n要立即重启吗?
|
||||
ShowReadmeCheck=是,我想查阅自述文件
|
||||
YesRadio=是,立即重启电脑(&Y)
|
||||
NoRadio=否,稍后重启电脑(&N)
|
||||
; used for example as 'Run MyProg.exe'
|
||||
RunEntryExec=运行 %1
|
||||
; used for example as 'View Readme.txt'
|
||||
RunEntryShellExec=查阅 %1
|
||||
|
||||
; *** “安装程序需要下一张磁盘”提示
|
||||
ChangeDiskTitle=安装程序需要下一张磁盘
|
||||
SelectDiskLabel2=请插入磁盘 %1 并点击“确定”。%n%n如果这个磁盘中的文件可以在下列文件夹之外的文件夹中找到,请输入正确的路径或点击“浏览”。
|
||||
PathLabel=路径(&P):
|
||||
FileNotInDir2=“%2”中找不到文件“%1”。请插入正确的磁盘或选择其他文件夹。
|
||||
SelectDirectoryLabel=请指定下一张磁盘的位置。
|
||||
|
||||
; *** 安装阶段消息
|
||||
SetupAborted=安装程序未完成安装。%n%n请修正这个问题并重新运行安装程序。
|
||||
AbortRetryIgnoreSelectAction=选择操作
|
||||
AbortRetryIgnoreRetry=重试(&T)
|
||||
AbortRetryIgnoreIgnore=忽略错误并继续(&I)
|
||||
AbortRetryIgnoreCancel=关闭安装程序
|
||||
RetryCancelSelectAction=选择操作
|
||||
RetryCancelRetry=重试(&T)
|
||||
RetryCancelCancel=取消(&C)
|
||||
|
||||
; *** 安装状态消息
|
||||
StatusClosingApplications=正在关闭应用程序...
|
||||
StatusCreateDirs=正在创建目录...
|
||||
StatusExtractFiles=正在提取文件...
|
||||
StatusDownloadFiles=正在下载文件...
|
||||
StatusCreateIcons=正在创建快捷方式...
|
||||
StatusCreateIniEntries=正在创建 INI 条目...
|
||||
StatusCreateRegistryEntries=正在创建注册表条目...
|
||||
StatusRegisterFiles=正在注册文件...
|
||||
StatusSavingUninstall=正在保存卸载信息...
|
||||
StatusRunProgram=正在完成安装...
|
||||
StatusRestartingApplications=正在重启应用程序...
|
||||
StatusRollback=正在撤销更改...
|
||||
|
||||
; *** 其他错误
|
||||
ErrorInternal2=内部错误:%1
|
||||
ErrorFunctionFailedNoCode=%1 失败
|
||||
ErrorFunctionFailed=%1 失败;错误代码 %2
|
||||
ErrorFunctionFailedWithMessage=%1 失败;错误代码 %2.%n%3
|
||||
ErrorExecutingProgram=无法执行文件:%n%1
|
||||
|
||||
; *** 注册表错误
|
||||
ErrorRegOpenKey=打开注册表项时出错:%n%1\%2
|
||||
ErrorRegCreateKey=创建注册表项时出错:%n%1\%2
|
||||
ErrorRegWriteKey=写入注册表项时出错:%n%1\%2
|
||||
|
||||
; *** INI 错误
|
||||
ErrorIniEntry=在文件“%1”中创建 INI 条目时出错。
|
||||
|
||||
; *** 文件复制错误
|
||||
FileAbortRetryIgnoreSkipNotRecommended=跳过此文件(&S) (不推荐)
|
||||
FileAbortRetryIgnoreIgnoreNotRecommended=忽略错误并继续(&I) (不推荐)
|
||||
SourceIsCorrupted=源文件已损坏
|
||||
SourceDoesntExist=源文件“%1”不存在
|
||||
SourceVerificationFailed=源文件验证失败: %1
|
||||
VerificationSignatureDoesntExist=签名文件“%1”不存在
|
||||
VerificationSignatureInvalid=签名文件“%1”无效
|
||||
VerificationKeyNotFound=签名文件“%1”使用了未知密钥
|
||||
VerificationFileNameIncorrect=文件名不正确
|
||||
VerificationFileTagIncorrect=文件标签不正确
|
||||
VerificationFileSizeIncorrect=文件大小不正确
|
||||
VerificationFileHashIncorrect=文件哈希值不正确
|
||||
ExistingFileReadOnly2=无法替换现有文件,它是只读的。
|
||||
ExistingFileReadOnlyRetry=移除只读属性并重试(&R)
|
||||
ExistingFileReadOnlyKeepExisting=保留现有文件(&K)
|
||||
ErrorReadingExistingDest=尝试读取现有文件时出错:
|
||||
FileExistsSelectAction=选择操作
|
||||
FileExists2=文件已经存在。
|
||||
FileExistsOverwriteExisting=覆盖已存在的文件(&O)
|
||||
FileExistsKeepExisting=保留现有的文件(&K)
|
||||
FileExistsOverwriteOrKeepAll=为所有冲突文件执行此操作(&D)
|
||||
ExistingFileNewerSelectAction=选择操作
|
||||
ExistingFileNewer2=现有的文件比安装程序将要安装的文件还要新。
|
||||
ExistingFileNewerOverwriteExisting=覆盖已存在的文件(&O)
|
||||
ExistingFileNewerKeepExisting=保留现有的文件(&K) (推荐)
|
||||
ExistingFileNewerOverwriteOrKeepAll=为所有冲突文件执行此操作(&D)
|
||||
ErrorChangingAttr=尝试更改下列现有文件的属性时出错:
|
||||
ErrorCreatingTemp=尝试在目标目录创建文件时出错:
|
||||
ErrorReadingSource=尝试读取下列源文件时出错:
|
||||
ErrorCopying=尝试复制下列文件时出错:
|
||||
ErrorDownloading=下载文件时出错:
|
||||
ErrorExtracting=解压压缩文件时出错:
|
||||
ErrorReplacingExistingFile=尝试替换现有文件时出错:
|
||||
ErrorRestartReplace=重启并替换失败:
|
||||
ErrorRenamingTemp=尝试重命名下列目标目录中的一个文件时出错:
|
||||
ErrorRegisterServer=无法注册 DLL/OCX:%1
|
||||
ErrorRegSvr32Failed=RegSvr32 失败;退出代码 %1
|
||||
ErrorRegisterTypeLib=无法注册类库:%1
|
||||
|
||||
; *** 卸载显示名字标记
|
||||
; used for example as 'My Program (32-bit)'
|
||||
UninstallDisplayNameMark=%1 (%2)
|
||||
; used for example as 'My Program (32-bit, All users)'
|
||||
UninstallDisplayNameMarks=%1 (%2, %3)
|
||||
UninstallDisplayNameMark32Bit=32 位
|
||||
UninstallDisplayNameMark64Bit=64 位
|
||||
UninstallDisplayNameMarkAllUsers=所有用户
|
||||
UninstallDisplayNameMarkCurrentUser=当前用户
|
||||
|
||||
; *** 安装后错误
|
||||
ErrorOpeningReadme=尝试打开自述文件时出错。
|
||||
ErrorRestartingComputer=安装程序无法重启电脑,请手动重启。
|
||||
|
||||
; *** 卸载消息
|
||||
UninstallNotFound=文件“%1”不存在。无法卸载。
|
||||
UninstallOpenError=文件“%1”不能被打开。无法卸载。
|
||||
UninstallUnsupportedVer=此版本的卸载程序无法识别卸载日志文件“%1”的格式。无法卸载
|
||||
UninstallUnknownEntry=卸载日志中遇到一个未知条目 (%1)
|
||||
ConfirmUninstall=您确认要完全移除 %1 及其所有组件吗?
|
||||
UninstallOnlyOnWin64=仅允许在 64 位 Windows 中卸载此程序。
|
||||
OnlyAdminCanUninstall=仅使用管理员权限的用户能完成此卸载。
|
||||
UninstallStatusLabel=正在从您的电脑中移除 %1,请稍候。
|
||||
UninstalledAll=已顺利从您的电脑中移除 %1。
|
||||
UninstalledMost=%1 卸载完成。%n%n有部分内容未能被删除,但您可以手动删除它们。
|
||||
UninstalledAndNeedsRestart=为完成 %1 的卸载,需要重启您的电脑。%n%n立即重启电脑吗?
|
||||
UninstallDataCorrupted=文件“%1”已损坏。无法卸载
|
||||
|
||||
; *** 卸载状态消息
|
||||
ConfirmDeleteSharedFileTitle=删除共享的文件吗?
|
||||
ConfirmDeleteSharedFile2=系统表示下列共享的文件已不有其他程序使用。您希望卸载程序删除这些共享的文件吗?%n%n如果删除这些文件,但仍有程序在使用这些文件,则这些程序可能出现异常。如果您不能确定,请选择“否”,在系统中保留这些文件以免引发问题。
|
||||
SharedFileNameLabel=文件名:
|
||||
SharedFileLocationLabel=位置:
|
||||
WizardUninstalling=卸载状态
|
||||
StatusUninstalling=正在卸载 %1...
|
||||
|
||||
; *** Shutdown block reasons
|
||||
ShutdownBlockReasonInstallingApp=正在安装 %1。
|
||||
ShutdownBlockReasonUninstallingApp=正在卸载 %1。
|
||||
|
||||
; The custom messages below aren't used by Setup itself, but if you make
|
||||
; use of them in your scripts, you'll want to translate them.
|
||||
|
||||
[CustomMessages]
|
||||
|
||||
NameAndVersion=%1 版本 %2
|
||||
AdditionalIcons=附加快捷方式:
|
||||
CreateDesktopIcon=创建桌面快捷方式(&D)
|
||||
CreateQuickLaunchIcon=创建快速启动栏快捷方式(&Q)
|
||||
ProgramOnTheWeb=%1 网站
|
||||
UninstallProgram=卸载 %1
|
||||
LaunchProgram=运行 %1
|
||||
AssocFileExtension=将 %2 文件扩展名与 %1 建立关联(&A)
|
||||
AssocingFileExtension=正在将 %2 文件扩展名与 %1 建立关联...
|
||||
AutoStartProgramGroupDescription=启动:
|
||||
AutoStartProgram=自动启动 %1
|
||||
AddonHostProgramNotFound=您选择的文件夹中无法找到 %1。%n%n您要继续吗?
|
||||
@@ -0,0 +1,90 @@
|
||||
; ============================================================================
|
||||
; smPlayer Windows Installer (Inno Setup 6)
|
||||
; ----------------------------------------------------------------------------
|
||||
; 用法:
|
||||
; 1. 先执行:flutter build windows --release
|
||||
; 2. 再执行:iscc installer\sm_emby.iss
|
||||
; 或一键: powershell -ExecutionPolicy Bypass -File scripts\build-installer.ps1
|
||||
;
|
||||
; 命令行可覆盖版本号:iscc /DAppVersion=1.2.3 installer\sm_emby.iss
|
||||
; ============================================================================
|
||||
|
||||
#define AppName "smPlayer"
|
||||
#define AppPublisher "smPlayer"
|
||||
#define AppExeName "smplayer.exe"
|
||||
#define AppId "{{A8E3F1C2-7D5B-4A6E-9C8D-3F2E1B0A9D7C}"
|
||||
|
||||
#ifndef AppVersion
|
||||
#define AppVersion "0.0.15"
|
||||
#endif
|
||||
|
||||
#define ProjectRoot SourcePath + ".."
|
||||
#define BuildDir ProjectRoot + "\build\windows\x64\runner\Release"
|
||||
#define IconFile ProjectRoot + "\windows\runner\resources\app_icon.ico"
|
||||
#define OutputDirAbs ProjectRoot + "\build\installer"
|
||||
|
||||
[Setup]
|
||||
AppId={#AppId}
|
||||
AppName={#AppName}
|
||||
AppVersion={#AppVersion}
|
||||
AppVerName={#AppName} {#AppVersion}
|
||||
AppPublisher={#AppPublisher}
|
||||
DefaultDirName={autopf}\{#AppName}
|
||||
DefaultGroupName={#AppName}
|
||||
DisableProgramGroupPage=yes
|
||||
DisableDirPage=no
|
||||
ArchitecturesInstallIn64BitMode=x64compatible
|
||||
ArchitecturesAllowed=x64compatible
|
||||
OutputDir={#OutputDirAbs}
|
||||
OutputBaseFilename=smplayer-setup-{#AppVersion}-x64
|
||||
SetupIconFile={#IconFile}
|
||||
UninstallDisplayIcon={app}\{#AppExeName}
|
||||
Compression=lzma2/ultra64
|
||||
SolidCompression=yes
|
||||
WizardStyle=modern
|
||||
PrivilegesRequired=admin
|
||||
PrivilegesRequiredOverridesAllowed=dialog
|
||||
CloseApplications=force
|
||||
RestartApplications=no
|
||||
MinVersion=10.0
|
||||
|
||||
[Languages]
|
||||
; ChineseSimplified.isl 来自 https://github.com/jrsoftware/issrc (Files/Languages/Unofficial)
|
||||
; 已随仓库提交至 installer/lang/ 以避免依赖系统级 Inno Setup 安装
|
||||
Name: "chinesesimp"; MessagesFile: "lang\ChineseSimplified.isl"
|
||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||
|
||||
[Tasks]
|
||||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
|
||||
|
||||
[Files]
|
||||
; 主可执行文件
|
||||
Source: "{#BuildDir}\{#AppExeName}"; DestDir: "{app}"; Flags: ignoreversion
|
||||
; 所有 DLL(Flutter 引擎、fvp/MDK、media_kit/mpv、插件等)
|
||||
Source: "{#BuildDir}\*.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||
; native_assets 清单
|
||||
Source: "{#BuildDir}\native_assets.json"; DestDir: "{app}"; Flags: ignoreversion skipifsourcedoesntexist
|
||||
; data 目录(flutter_assets + icudtl.dat + app.so)
|
||||
Source: "{#BuildDir}\data\*"; DestDir: "{app}\data"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
|
||||
[Icons]
|
||||
Name: "{group}\{#AppName}"; Filename: "{app}\{#AppExeName}"
|
||||
Name: "{group}\{cm:UninstallProgram,{#AppName}}"; Filename: "{uninstallexe}"
|
||||
Name: "{autodesktop}\{#AppName}"; Filename: "{app}\{#AppExeName}"; Tasks: desktopicon
|
||||
|
||||
[Registry]
|
||||
; 自定义 URL scheme:smplayer:// → 本程序(Trakt OAuth 回跳 smplayer://oauth-trakt)。
|
||||
; 安装到 HKLM\Software\Classes,正式安装走管理员权限写入。
|
||||
; app 已运行时点链接会拉起第二个进程,由 windows/runner/main.cpp 的 SendAppLinkToInstance
|
||||
; 把深链转发给已存在窗口后立即退出(app_links 官方单实例范式),不会出现孤立的第二实例。
|
||||
Root: HKLM; Subkey: "Software\Classes\smplayer"; ValueType: string; ValueName: ""; ValueData: "URL:smplayer Protocol"; Flags: uninsdeletekey
|
||||
Root: HKLM; Subkey: "Software\Classes\smplayer"; ValueType: string; ValueName: "URL Protocol"; ValueData: ""
|
||||
Root: HKLM; Subkey: "Software\Classes\smplayer\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#AppExeName},0"
|
||||
Root: HKLM; Subkey: "Software\Classes\smplayer\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}"" ""%1"""
|
||||
|
||||
[Run]
|
||||
Filename: "{app}\{#AppExeName}"; Description: "{cm:LaunchProgram,{#AppName}}"; Flags: nowait postinstall skipifsilent
|
||||
|
||||
[UninstallDelete]
|
||||
; 清理用户数据目录(可选;如需保留用户配置可注释掉)
|
||||
; Type: filesandordirs; Name: "{userappdata}\sm_emby"
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart'
|
||||
show defaultTargetPlatform, TargetPlatform;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:forui/forui.dart';
|
||||
|
||||
import 'features/update/update_dialog.dart';
|
||||
import 'providers/appearance_provider.dart';
|
||||
import 'providers/di_providers.dart';
|
||||
import 'providers/server_sync_provider.dart';
|
||||
import 'providers/sm_account_provider.dart';
|
||||
import 'providers/update_provider.dart';
|
||||
import 'router/app_router.dart' show appRouterProvider, rootNavigatorKey;
|
||||
import 'shared/theme/app_scroll_behavior.dart';
|
||||
import 'shared/theme/app_theme.dart';
|
||||
import 'shared/utils/app_logger.dart';
|
||||
|
||||
class SmPlayerApp extends ConsumerStatefulWidget {
|
||||
const SmPlayerApp({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<SmPlayerApp> createState() => _SmPlayerAppState();
|
||||
}
|
||||
|
||||
class _SmPlayerAppState extends ConsumerState<SmPlayerApp> {
|
||||
bool _updateDialogShown = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
try {
|
||||
final queue = await ref.read(traktSyncQueueProvider.future);
|
||||
await queue.drain();
|
||||
} catch (e) {
|
||||
AppLogger.debug('Trakt', 'startup drain skipped', e);
|
||||
}
|
||||
unawaited(ref.read(updateProvider.notifier).silentCheck());
|
||||
unawaited(_autoSyncPull());
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _autoSyncPull() async {
|
||||
try {
|
||||
final account = await ref.read(smAccountProvider.future);
|
||||
if (!account.hasVip) return;
|
||||
final outcome = await ref.read(serverSyncProvider.notifier).pull();
|
||||
if (outcome.isError) {
|
||||
AppLogger.debug('CloudSync', 'auto-pull failed: ${outcome.message}');
|
||||
}
|
||||
} catch (e) {
|
||||
AppLogger.debug('CloudSync', 'auto-pull skipped', e);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final router = ref.watch(appRouterProvider);
|
||||
final appearance = ref.watch(appearanceProvider);
|
||||
final flutterMode = appearance.maybeWhen(
|
||||
data: (d) => d.themeMode,
|
||||
orElse: () => ThemeMode.system,
|
||||
);
|
||||
|
||||
return MaterialApp.router(
|
||||
title: 'smPlayer',
|
||||
debugShowCheckedModeBanner: false,
|
||||
locale: const Locale('zh', 'CN'),
|
||||
supportedLocales: const [Locale('zh', 'CN'), Locale('en')],
|
||||
localizationsDelegates: const [
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
],
|
||||
theme: AppTheme.light(),
|
||||
darkTheme: AppTheme.dark(),
|
||||
themeMode: flutterMode,
|
||||
scrollBehavior: const AppScrollBehavior(),
|
||||
routerConfig: router,
|
||||
builder: (context, child) {
|
||||
final zinc = Theme.of(context).brightness == Brightness.dark
|
||||
? FThemes.zinc.dark
|
||||
: FThemes.zinc.light;
|
||||
return FTheme(
|
||||
data: _isTouchPlatform ? zinc.touch : zinc.desktop,
|
||||
child: FToaster(
|
||||
child: Consumer(
|
||||
builder: (context, ref, _) {
|
||||
ref.listen<UpdateState>(updateProvider, (previous, next) {
|
||||
if (_updateDialogShown ||
|
||||
next.status != UpdateStatus.available ||
|
||||
!next.silent) {
|
||||
return;
|
||||
}
|
||||
_updateDialogShown = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
final navContext = rootNavigatorKey.currentContext;
|
||||
if (navContext == null) return;
|
||||
showFDialog(
|
||||
context: navContext,
|
||||
builder: (_, _, _) => const UpdateDialog(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
return child ?? const SizedBox();
|
||||
},
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
bool get _isTouchPlatform => switch (defaultTargetPlatform) {
|
||||
TargetPlatform.android ||
|
||||
TargetPlatform.iOS ||
|
||||
TargetPlatform.fuchsia => true,
|
||||
_ => false,
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
class AppUpdateAsset {
|
||||
final String name;
|
||||
final String downloadUrl;
|
||||
final int size;
|
||||
final String? sha256;
|
||||
|
||||
|
||||
final String? version;
|
||||
|
||||
const AppUpdateAsset({
|
||||
required this.name,
|
||||
required this.downloadUrl,
|
||||
required this.size,
|
||||
this.sha256,
|
||||
this.version,
|
||||
});
|
||||
|
||||
factory AppUpdateAsset.fromJson(Map<String, dynamic> json) {
|
||||
return AppUpdateAsset(
|
||||
name: json['name']?.toString() ?? '',
|
||||
downloadUrl:
|
||||
json['browser_download_url']?.toString() ??
|
||||
json['browserDownloadUrl']?.toString() ??
|
||||
json['url']?.toString() ??
|
||||
'',
|
||||
size: json['size'] is int ? json['size'] as int : 0,
|
||||
sha256: (json['sha256']?.toString().trim().isEmpty ?? true)
|
||||
? null
|
||||
: json['sha256'].toString().trim().toLowerCase(),
|
||||
version: (json['version']?.toString().trim().isEmpty ?? true)
|
||||
? null
|
||||
: json['version'].toString().trim(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AppUpdateInfo {
|
||||
final String version;
|
||||
final String releaseNotes;
|
||||
final Uri htmlUrl;
|
||||
final List<AppUpdateAsset> assets;
|
||||
final Map<String, AppUpdateAsset> platformAssets;
|
||||
final AppUpdateAsset? selectedAsset;
|
||||
final bool noAssetForPlatform;
|
||||
|
||||
const AppUpdateInfo({
|
||||
required this.version,
|
||||
required this.releaseNotes,
|
||||
required this.htmlUrl,
|
||||
required this.assets,
|
||||
this.platformAssets = const {},
|
||||
this.selectedAsset,
|
||||
this.noAssetForPlatform = false,
|
||||
});
|
||||
|
||||
AppUpdateInfo copyWith({
|
||||
String? version,
|
||||
String? releaseNotes,
|
||||
Uri? htmlUrl,
|
||||
List<AppUpdateAsset>? assets,
|
||||
Map<String, AppUpdateAsset>? platformAssets,
|
||||
AppUpdateAsset? selectedAsset,
|
||||
bool? noAssetForPlatform,
|
||||
}) {
|
||||
return AppUpdateInfo(
|
||||
version: version ?? this.version,
|
||||
releaseNotes: releaseNotes ?? this.releaseNotes,
|
||||
htmlUrl: htmlUrl ?? this.htmlUrl,
|
||||
assets: assets ?? this.assets,
|
||||
platformAssets: platformAssets ?? this.platformAssets,
|
||||
selectedAsset: selectedAsset ?? this.selectedAsset,
|
||||
noAssetForPlatform: noAssetForPlatform ?? this.noAssetForPlatform,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UpdateCheckResult {
|
||||
final bool hasUpdate;
|
||||
final AppUpdateInfo? info;
|
||||
|
||||
const UpdateCheckResult({required this.hasUpdate, this.info});
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'auth.freezed.dart';
|
||||
part 'auth.g.dart';
|
||||
|
||||
@freezed
|
||||
abstract class AuthByNameReq with _$AuthByNameReq {
|
||||
const factory AuthByNameReq({
|
||||
required String serverId,
|
||||
required String username,
|
||||
required String password,
|
||||
}) = _AuthByNameReq;
|
||||
|
||||
factory AuthByNameReq.fromJson(Map<String, dynamic> json) =>
|
||||
_$AuthByNameReqFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class SessionData with _$SessionData {
|
||||
const factory SessionData({
|
||||
required String serverId,
|
||||
required String token,
|
||||
required String userId,
|
||||
required String userName,
|
||||
@JsonKey(includeIfNull: false) String? password,
|
||||
required String createdAt,
|
||||
}) = _SessionData;
|
||||
|
||||
factory SessionData.fromJson(Map<String, dynamic> json) =>
|
||||
_$SessionDataFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class AuthedSession with _$AuthedSession {
|
||||
const factory AuthedSession({
|
||||
required String serverId,
|
||||
required String serverName,
|
||||
required String serverUrl,
|
||||
required String token,
|
||||
required String userId,
|
||||
required String userName,
|
||||
@JsonKey(includeIfNull: false) String? password,
|
||||
required bool isActive,
|
||||
}) = _AuthedSession;
|
||||
|
||||
factory AuthedSession.fromJson(Map<String, dynamic> json) =>
|
||||
_$AuthedSessionFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class SessionListRes with _$SessionListRes {
|
||||
const factory SessionListRes({
|
||||
required List<AuthedSession> sessions,
|
||||
|
||||
|
||||
String? activeServerId,
|
||||
}) = _SessionListRes;
|
||||
|
||||
factory SessionListRes.fromJson(Map<String, dynamic> json) =>
|
||||
_$SessionListResFromJson(json);
|
||||
}
|
||||
Generated
+1119
File diff suppressed because it is too large
Load Diff
Generated
+78
@@ -0,0 +1,78 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'auth.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_AuthByNameReq _$AuthByNameReqFromJson(Map<String, dynamic> json) =>
|
||||
_AuthByNameReq(
|
||||
serverId: json['serverId'] as String,
|
||||
username: json['username'] as String,
|
||||
password: json['password'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$AuthByNameReqToJson(_AuthByNameReq instance) =>
|
||||
<String, dynamic>{
|
||||
'serverId': instance.serverId,
|
||||
'username': instance.username,
|
||||
'password': instance.password,
|
||||
};
|
||||
|
||||
_SessionData _$SessionDataFromJson(Map<String, dynamic> json) => _SessionData(
|
||||
serverId: json['serverId'] as String,
|
||||
token: json['token'] as String,
|
||||
userId: json['userId'] as String,
|
||||
userName: json['userName'] as String,
|
||||
password: json['password'] as String?,
|
||||
createdAt: json['createdAt'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SessionDataToJson(_SessionData instance) =>
|
||||
<String, dynamic>{
|
||||
'serverId': instance.serverId,
|
||||
'token': instance.token,
|
||||
'userId': instance.userId,
|
||||
'userName': instance.userName,
|
||||
'password': ?instance.password,
|
||||
'createdAt': instance.createdAt,
|
||||
};
|
||||
|
||||
_AuthedSession _$AuthedSessionFromJson(Map<String, dynamic> json) =>
|
||||
_AuthedSession(
|
||||
serverId: json['serverId'] as String,
|
||||
serverName: json['serverName'] as String,
|
||||
serverUrl: json['serverUrl'] as String,
|
||||
token: json['token'] as String,
|
||||
userId: json['userId'] as String,
|
||||
userName: json['userName'] as String,
|
||||
password: json['password'] as String?,
|
||||
isActive: json['isActive'] as bool,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$AuthedSessionToJson(_AuthedSession instance) =>
|
||||
<String, dynamic>{
|
||||
'serverId': instance.serverId,
|
||||
'serverName': instance.serverName,
|
||||
'serverUrl': instance.serverUrl,
|
||||
'token': instance.token,
|
||||
'userId': instance.userId,
|
||||
'userName': instance.userName,
|
||||
'password': ?instance.password,
|
||||
'isActive': instance.isActive,
|
||||
};
|
||||
|
||||
_SessionListRes _$SessionListResFromJson(Map<String, dynamic> json) =>
|
||||
_SessionListRes(
|
||||
sessions: (json['sessions'] as List<dynamic>)
|
||||
.map((e) => AuthedSession.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
activeServerId: json['activeServerId'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SessionListResToJson(_SessionListRes instance) =>
|
||||
<String, dynamic>{
|
||||
'sessions': instance.sessions,
|
||||
'activeServerId': instance.activeServerId,
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import 'auth.dart';
|
||||
import 'danmaku.dart';
|
||||
import 'script_widget.dart';
|
||||
import 'server.dart';
|
||||
|
||||
part 'backup.freezed.dart';
|
||||
part 'backup.g.dart';
|
||||
|
||||
|
||||
@freezed
|
||||
abstract class BackupBundle with _$BackupBundle {
|
||||
const factory BackupBundle({
|
||||
required int version,
|
||||
required String exportedAt,
|
||||
required List<EmbyServer> servers,
|
||||
required List<SessionData> sessions,
|
||||
@JsonKey(fromJson: _danmakuSourcesFromJson, toJson: _danmakuSourcesToJson)
|
||||
@Default(<DanmakuSource>[])
|
||||
List<DanmakuSource> danmakuSources,
|
||||
@Default(<InstalledScriptWidget>[])
|
||||
List<InstalledScriptWidget> scriptWidgets,
|
||||
@Default(<ScriptWidgetSubscription>[])
|
||||
List<ScriptWidgetSubscription> scriptWidgetSubscriptions,
|
||||
}) = _BackupBundle;
|
||||
|
||||
factory BackupBundle.fromJson(Map<String, dynamic> json) =>
|
||||
_$BackupBundleFromJson(json);
|
||||
}
|
||||
|
||||
List<DanmakuSource> _danmakuSourcesFromJson(Object? raw) {
|
||||
if (raw is! List) return const [];
|
||||
final out = <DanmakuSource>[];
|
||||
for (final item in raw) {
|
||||
if (item is Map<String, dynamic>) {
|
||||
final source = DanmakuSource.fromJson(item);
|
||||
if (source.url.isNotEmpty) out.add(source);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> _danmakuSourcesToJson(List<DanmakuSource> sources) {
|
||||
return [
|
||||
for (final s in sources)
|
||||
if (s.url.trim().isNotEmpty) {'url': s.url.trim(), 'name': s.name},
|
||||
];
|
||||
}
|
||||
Generated
+325
@@ -0,0 +1,325 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'backup.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$BackupBundle {
|
||||
|
||||
int get version; String get exportedAt; List<EmbyServer> get servers; List<SessionData> get sessions;@JsonKey(fromJson: _danmakuSourcesFromJson, toJson: _danmakuSourcesToJson) List<DanmakuSource> get danmakuSources; List<InstalledScriptWidget> get scriptWidgets; List<ScriptWidgetSubscription> get scriptWidgetSubscriptions;
|
||||
/// Create a copy of BackupBundle
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$BackupBundleCopyWith<BackupBundle> get copyWith => _$BackupBundleCopyWithImpl<BackupBundle>(this as BackupBundle, _$identity);
|
||||
|
||||
/// Serializes this BackupBundle to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is BackupBundle&&(identical(other.version, version) || other.version == version)&&(identical(other.exportedAt, exportedAt) || other.exportedAt == exportedAt)&&const DeepCollectionEquality().equals(other.servers, servers)&&const DeepCollectionEquality().equals(other.sessions, sessions)&&const DeepCollectionEquality().equals(other.danmakuSources, danmakuSources)&&const DeepCollectionEquality().equals(other.scriptWidgets, scriptWidgets)&&const DeepCollectionEquality().equals(other.scriptWidgetSubscriptions, scriptWidgetSubscriptions));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,version,exportedAt,const DeepCollectionEquality().hash(servers),const DeepCollectionEquality().hash(sessions),const DeepCollectionEquality().hash(danmakuSources),const DeepCollectionEquality().hash(scriptWidgets),const DeepCollectionEquality().hash(scriptWidgetSubscriptions));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'BackupBundle(version: $version, exportedAt: $exportedAt, servers: $servers, sessions: $sessions, danmakuSources: $danmakuSources, scriptWidgets: $scriptWidgets, scriptWidgetSubscriptions: $scriptWidgetSubscriptions)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $BackupBundleCopyWith<$Res> {
|
||||
factory $BackupBundleCopyWith(BackupBundle value, $Res Function(BackupBundle) _then) = _$BackupBundleCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
int version, String exportedAt, List<EmbyServer> servers, List<SessionData> sessions,@JsonKey(fromJson: _danmakuSourcesFromJson, toJson: _danmakuSourcesToJson) List<DanmakuSource> danmakuSources, List<InstalledScriptWidget> scriptWidgets, List<ScriptWidgetSubscription> scriptWidgetSubscriptions
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$BackupBundleCopyWithImpl<$Res>
|
||||
implements $BackupBundleCopyWith<$Res> {
|
||||
_$BackupBundleCopyWithImpl(this._self, this._then);
|
||||
|
||||
final BackupBundle _self;
|
||||
final $Res Function(BackupBundle) _then;
|
||||
|
||||
/// Create a copy of BackupBundle
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? version = null,Object? exportedAt = null,Object? servers = null,Object? sessions = null,Object? danmakuSources = null,Object? scriptWidgets = null,Object? scriptWidgetSubscriptions = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
version: null == version ? _self.version : version // ignore: cast_nullable_to_non_nullable
|
||||
as int,exportedAt: null == exportedAt ? _self.exportedAt : exportedAt // ignore: cast_nullable_to_non_nullable
|
||||
as String,servers: null == servers ? _self.servers : servers // ignore: cast_nullable_to_non_nullable
|
||||
as List<EmbyServer>,sessions: null == sessions ? _self.sessions : sessions // ignore: cast_nullable_to_non_nullable
|
||||
as List<SessionData>,danmakuSources: null == danmakuSources ? _self.danmakuSources : danmakuSources // ignore: cast_nullable_to_non_nullable
|
||||
as List<DanmakuSource>,scriptWidgets: null == scriptWidgets ? _self.scriptWidgets : scriptWidgets // ignore: cast_nullable_to_non_nullable
|
||||
as List<InstalledScriptWidget>,scriptWidgetSubscriptions: null == scriptWidgetSubscriptions ? _self.scriptWidgetSubscriptions : scriptWidgetSubscriptions // ignore: cast_nullable_to_non_nullable
|
||||
as List<ScriptWidgetSubscription>,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [BackupBundle].
|
||||
extension BackupBundlePatterns on BackupBundle {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _BackupBundle value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _BackupBundle() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _BackupBundle value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _BackupBundle():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _BackupBundle value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _BackupBundle() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( int version, String exportedAt, List<EmbyServer> servers, List<SessionData> sessions, @JsonKey(fromJson: _danmakuSourcesFromJson, toJson: _danmakuSourcesToJson) List<DanmakuSource> danmakuSources, List<InstalledScriptWidget> scriptWidgets, List<ScriptWidgetSubscription> scriptWidgetSubscriptions)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _BackupBundle() when $default != null:
|
||||
return $default(_that.version,_that.exportedAt,_that.servers,_that.sessions,_that.danmakuSources,_that.scriptWidgets,_that.scriptWidgetSubscriptions);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( int version, String exportedAt, List<EmbyServer> servers, List<SessionData> sessions, @JsonKey(fromJson: _danmakuSourcesFromJson, toJson: _danmakuSourcesToJson) List<DanmakuSource> danmakuSources, List<InstalledScriptWidget> scriptWidgets, List<ScriptWidgetSubscription> scriptWidgetSubscriptions) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _BackupBundle():
|
||||
return $default(_that.version,_that.exportedAt,_that.servers,_that.sessions,_that.danmakuSources,_that.scriptWidgets,_that.scriptWidgetSubscriptions);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( int version, String exportedAt, List<EmbyServer> servers, List<SessionData> sessions, @JsonKey(fromJson: _danmakuSourcesFromJson, toJson: _danmakuSourcesToJson) List<DanmakuSource> danmakuSources, List<InstalledScriptWidget> scriptWidgets, List<ScriptWidgetSubscription> scriptWidgetSubscriptions)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _BackupBundle() when $default != null:
|
||||
return $default(_that.version,_that.exportedAt,_that.servers,_that.sessions,_that.danmakuSources,_that.scriptWidgets,_that.scriptWidgetSubscriptions);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _BackupBundle implements BackupBundle {
|
||||
const _BackupBundle({required this.version, required this.exportedAt, required final List<EmbyServer> servers, required final List<SessionData> sessions, @JsonKey(fromJson: _danmakuSourcesFromJson, toJson: _danmakuSourcesToJson) final List<DanmakuSource> danmakuSources = const <DanmakuSource>[], final List<InstalledScriptWidget> scriptWidgets = const <InstalledScriptWidget>[], final List<ScriptWidgetSubscription> scriptWidgetSubscriptions = const <ScriptWidgetSubscription>[]}): _servers = servers,_sessions = sessions,_danmakuSources = danmakuSources,_scriptWidgets = scriptWidgets,_scriptWidgetSubscriptions = scriptWidgetSubscriptions;
|
||||
factory _BackupBundle.fromJson(Map<String, dynamic> json) => _$BackupBundleFromJson(json);
|
||||
|
||||
@override final int version;
|
||||
@override final String exportedAt;
|
||||
final List<EmbyServer> _servers;
|
||||
@override List<EmbyServer> get servers {
|
||||
if (_servers is EqualUnmodifiableListView) return _servers;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_servers);
|
||||
}
|
||||
|
||||
final List<SessionData> _sessions;
|
||||
@override List<SessionData> get sessions {
|
||||
if (_sessions is EqualUnmodifiableListView) return _sessions;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_sessions);
|
||||
}
|
||||
|
||||
final List<DanmakuSource> _danmakuSources;
|
||||
@override@JsonKey(fromJson: _danmakuSourcesFromJson, toJson: _danmakuSourcesToJson) List<DanmakuSource> get danmakuSources {
|
||||
if (_danmakuSources is EqualUnmodifiableListView) return _danmakuSources;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_danmakuSources);
|
||||
}
|
||||
|
||||
final List<InstalledScriptWidget> _scriptWidgets;
|
||||
@override@JsonKey() List<InstalledScriptWidget> get scriptWidgets {
|
||||
if (_scriptWidgets is EqualUnmodifiableListView) return _scriptWidgets;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_scriptWidgets);
|
||||
}
|
||||
|
||||
final List<ScriptWidgetSubscription> _scriptWidgetSubscriptions;
|
||||
@override@JsonKey() List<ScriptWidgetSubscription> get scriptWidgetSubscriptions {
|
||||
if (_scriptWidgetSubscriptions is EqualUnmodifiableListView) return _scriptWidgetSubscriptions;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_scriptWidgetSubscriptions);
|
||||
}
|
||||
|
||||
|
||||
/// Create a copy of BackupBundle
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$BackupBundleCopyWith<_BackupBundle> get copyWith => __$BackupBundleCopyWithImpl<_BackupBundle>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$BackupBundleToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _BackupBundle&&(identical(other.version, version) || other.version == version)&&(identical(other.exportedAt, exportedAt) || other.exportedAt == exportedAt)&&const DeepCollectionEquality().equals(other._servers, _servers)&&const DeepCollectionEquality().equals(other._sessions, _sessions)&&const DeepCollectionEquality().equals(other._danmakuSources, _danmakuSources)&&const DeepCollectionEquality().equals(other._scriptWidgets, _scriptWidgets)&&const DeepCollectionEquality().equals(other._scriptWidgetSubscriptions, _scriptWidgetSubscriptions));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,version,exportedAt,const DeepCollectionEquality().hash(_servers),const DeepCollectionEquality().hash(_sessions),const DeepCollectionEquality().hash(_danmakuSources),const DeepCollectionEquality().hash(_scriptWidgets),const DeepCollectionEquality().hash(_scriptWidgetSubscriptions));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'BackupBundle(version: $version, exportedAt: $exportedAt, servers: $servers, sessions: $sessions, danmakuSources: $danmakuSources, scriptWidgets: $scriptWidgets, scriptWidgetSubscriptions: $scriptWidgetSubscriptions)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$BackupBundleCopyWith<$Res> implements $BackupBundleCopyWith<$Res> {
|
||||
factory _$BackupBundleCopyWith(_BackupBundle value, $Res Function(_BackupBundle) _then) = __$BackupBundleCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
int version, String exportedAt, List<EmbyServer> servers, List<SessionData> sessions,@JsonKey(fromJson: _danmakuSourcesFromJson, toJson: _danmakuSourcesToJson) List<DanmakuSource> danmakuSources, List<InstalledScriptWidget> scriptWidgets, List<ScriptWidgetSubscription> scriptWidgetSubscriptions
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$BackupBundleCopyWithImpl<$Res>
|
||||
implements _$BackupBundleCopyWith<$Res> {
|
||||
__$BackupBundleCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _BackupBundle _self;
|
||||
final $Res Function(_BackupBundle) _then;
|
||||
|
||||
/// Create a copy of BackupBundle
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? version = null,Object? exportedAt = null,Object? servers = null,Object? sessions = null,Object? danmakuSources = null,Object? scriptWidgets = null,Object? scriptWidgetSubscriptions = null,}) {
|
||||
return _then(_BackupBundle(
|
||||
version: null == version ? _self.version : version // ignore: cast_nullable_to_non_nullable
|
||||
as int,exportedAt: null == exportedAt ? _self.exportedAt : exportedAt // ignore: cast_nullable_to_non_nullable
|
||||
as String,servers: null == servers ? _self._servers : servers // ignore: cast_nullable_to_non_nullable
|
||||
as List<EmbyServer>,sessions: null == sessions ? _self._sessions : sessions // ignore: cast_nullable_to_non_nullable
|
||||
as List<SessionData>,danmakuSources: null == danmakuSources ? _self._danmakuSources : danmakuSources // ignore: cast_nullable_to_non_nullable
|
||||
as List<DanmakuSource>,scriptWidgets: null == scriptWidgets ? _self._scriptWidgets : scriptWidgets // ignore: cast_nullable_to_non_nullable
|
||||
as List<InstalledScriptWidget>,scriptWidgetSubscriptions: null == scriptWidgetSubscriptions ? _self._scriptWidgetSubscriptions : scriptWidgetSubscriptions // ignore: cast_nullable_to_non_nullable
|
||||
as List<ScriptWidgetSubscription>,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
Generated
+50
@@ -0,0 +1,50 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'backup.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_BackupBundle _$BackupBundleFromJson(Map<String, dynamic> json) =>
|
||||
_BackupBundle(
|
||||
version: (json['version'] as num).toInt(),
|
||||
exportedAt: json['exportedAt'] as String,
|
||||
servers: (json['servers'] as List<dynamic>)
|
||||
.map((e) => EmbyServer.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
sessions: (json['sessions'] as List<dynamic>)
|
||||
.map((e) => SessionData.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
danmakuSources: json['danmakuSources'] == null
|
||||
? const <DanmakuSource>[]
|
||||
: _danmakuSourcesFromJson(json['danmakuSources']),
|
||||
scriptWidgets:
|
||||
(json['scriptWidgets'] as List<dynamic>?)
|
||||
?.map(
|
||||
(e) =>
|
||||
InstalledScriptWidget.fromJson(e as Map<String, dynamic>),
|
||||
)
|
||||
.toList() ??
|
||||
const <InstalledScriptWidget>[],
|
||||
scriptWidgetSubscriptions:
|
||||
(json['scriptWidgetSubscriptions'] as List<dynamic>?)
|
||||
?.map(
|
||||
(e) => ScriptWidgetSubscription.fromJson(
|
||||
e as Map<String, dynamic>,
|
||||
),
|
||||
)
|
||||
.toList() ??
|
||||
const <ScriptWidgetSubscription>[],
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$BackupBundleToJson(_BackupBundle instance) =>
|
||||
<String, dynamic>{
|
||||
'version': instance.version,
|
||||
'exportedAt': instance.exportedAt,
|
||||
'servers': instance.servers,
|
||||
'sessions': instance.sessions,
|
||||
'danmakuSources': _danmakuSourcesToJson(instance.danmakuSources),
|
||||
'scriptWidgets': instance.scriptWidgets,
|
||||
'scriptWidgetSubscriptions': instance.scriptWidgetSubscriptions,
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import 'dart:async';
|
||||
|
||||
class CancelledException implements Exception {
|
||||
final String? reason;
|
||||
const CancelledException([this.reason]);
|
||||
@override
|
||||
String toString() =>
|
||||
reason == null ? 'CancelledException' : 'CancelledException: $reason';
|
||||
}
|
||||
|
||||
class CancellationToken {
|
||||
final Completer<void> _completer = Completer<void>();
|
||||
String? _reason;
|
||||
|
||||
bool get isCancelled => _completer.isCompleted;
|
||||
String? get reason => _reason;
|
||||
Future<void> get whenCancelled => _completer.future;
|
||||
|
||||
void cancel([String? reason]) {
|
||||
if (_completer.isCompleted) return;
|
||||
_reason = reason;
|
||||
_completer.complete();
|
||||
}
|
||||
|
||||
void throwIfCancelled() {
|
||||
if (isCancelled) throw CancelledException(_reason);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export 'auth.dart';
|
||||
export 'backup.dart';
|
||||
export 'cancellation.dart';
|
||||
export 'error.dart';
|
||||
export 'library.dart';
|
||||
export 'playback.dart';
|
||||
export 'server.dart';
|
||||
export 'tmdb.dart';
|
||||
export 'danmaku.dart';
|
||||
export 'icon_library.dart';
|
||||
@@ -0,0 +1,354 @@
|
||||
import 'package:flutter/painting.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'danmaku.freezed.dart';
|
||||
part 'danmaku.g.dart';
|
||||
|
||||
|
||||
class DanmakuPanelSettings {
|
||||
final bool enabled;
|
||||
final double opacity;
|
||||
final double area;
|
||||
final double fontSize;
|
||||
final double speed;
|
||||
final double offset;
|
||||
final bool showTop;
|
||||
final bool showScroll;
|
||||
final bool showBottom;
|
||||
final bool colored;
|
||||
final double strokeWidth;
|
||||
final bool bold;
|
||||
final String? fontFamily;
|
||||
final double lineHeight;
|
||||
final bool fixedToScroll;
|
||||
final int maxRows;
|
||||
final bool heatmapEnabled;
|
||||
|
||||
const DanmakuPanelSettings({
|
||||
this.enabled = true,
|
||||
this.opacity = 1.0,
|
||||
this.area = 0.85,
|
||||
this.fontSize = 20,
|
||||
this.speed = 1.0,
|
||||
this.offset = 0.0,
|
||||
this.showTop = true,
|
||||
this.showScroll = true,
|
||||
this.showBottom = false,
|
||||
this.colored = true,
|
||||
this.strokeWidth = 1.5,
|
||||
this.bold = false,
|
||||
this.fontFamily,
|
||||
this.lineHeight = 1.6,
|
||||
this.fixedToScroll = false,
|
||||
this.maxRows = 0,
|
||||
this.heatmapEnabled = true,
|
||||
});
|
||||
|
||||
DanmakuPanelSettings copyWith({
|
||||
bool? enabled,
|
||||
double? opacity,
|
||||
double? area,
|
||||
double? fontSize,
|
||||
double? speed,
|
||||
double? offset,
|
||||
bool? showTop,
|
||||
bool? showScroll,
|
||||
bool? showBottom,
|
||||
bool? colored,
|
||||
double? strokeWidth,
|
||||
bool? bold,
|
||||
Object? fontFamily = _sentinel,
|
||||
double? lineHeight,
|
||||
bool? fixedToScroll,
|
||||
int? maxRows,
|
||||
bool? heatmapEnabled,
|
||||
}) => DanmakuPanelSettings(
|
||||
enabled: enabled ?? this.enabled,
|
||||
opacity: opacity ?? this.opacity,
|
||||
area: area ?? this.area,
|
||||
fontSize: fontSize ?? this.fontSize,
|
||||
speed: speed ?? this.speed,
|
||||
offset: offset ?? this.offset,
|
||||
showTop: showTop ?? this.showTop,
|
||||
showScroll: showScroll ?? this.showScroll,
|
||||
showBottom: showBottom ?? this.showBottom,
|
||||
colored: colored ?? this.colored,
|
||||
strokeWidth: strokeWidth ?? this.strokeWidth,
|
||||
bold: bold ?? this.bold,
|
||||
fontFamily: fontFamily == _sentinel
|
||||
? this.fontFamily
|
||||
: fontFamily as String?,
|
||||
lineHeight: lineHeight ?? this.lineHeight,
|
||||
fixedToScroll: fixedToScroll ?? this.fixedToScroll,
|
||||
maxRows: maxRows ?? this.maxRows,
|
||||
heatmapEnabled: heatmapEnabled ?? this.heatmapEnabled,
|
||||
);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
other is DanmakuPanelSettings &&
|
||||
enabled == other.enabled &&
|
||||
opacity == other.opacity &&
|
||||
area == other.area &&
|
||||
fontSize == other.fontSize &&
|
||||
speed == other.speed &&
|
||||
offset == other.offset &&
|
||||
showTop == other.showTop &&
|
||||
showScroll == other.showScroll &&
|
||||
showBottom == other.showBottom &&
|
||||
colored == other.colored &&
|
||||
strokeWidth == other.strokeWidth &&
|
||||
bold == other.bold &&
|
||||
fontFamily == other.fontFamily &&
|
||||
lineHeight == other.lineHeight &&
|
||||
fixedToScroll == other.fixedToScroll &&
|
||||
maxRows == other.maxRows &&
|
||||
heatmapEnabled == other.heatmapEnabled;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
enabled,
|
||||
opacity,
|
||||
area,
|
||||
fontSize,
|
||||
speed,
|
||||
offset,
|
||||
showTop,
|
||||
showScroll,
|
||||
showBottom,
|
||||
colored,
|
||||
strokeWidth,
|
||||
bold,
|
||||
fontFamily,
|
||||
lineHeight,
|
||||
fixedToScroll,
|
||||
maxRows,
|
||||
heatmapEnabled,
|
||||
);
|
||||
}
|
||||
|
||||
const Object _sentinel = Object();
|
||||
|
||||
class DanmakuSource {
|
||||
final String id;
|
||||
final String name;
|
||||
final String url;
|
||||
final bool enabled;
|
||||
|
||||
const DanmakuSource({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.url,
|
||||
this.enabled = true,
|
||||
});
|
||||
|
||||
DanmakuSource copyWith({
|
||||
String? id,
|
||||
String? name,
|
||||
String? url,
|
||||
bool? enabled,
|
||||
}) => DanmakuSource(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
url: url ?? this.url,
|
||||
enabled: enabled ?? this.enabled,
|
||||
);
|
||||
|
||||
String get displayName {
|
||||
final trimmed = name.trim();
|
||||
if (trimmed.isNotEmpty) return trimmed;
|
||||
return url;
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'url': url,
|
||||
'enabled': enabled,
|
||||
};
|
||||
|
||||
factory DanmakuSource.fromJson(Map<String, dynamic> json) {
|
||||
final url = (json['url'] ?? '').toString().trim();
|
||||
return DanmakuSource(
|
||||
id: (json['id'] ?? url).toString(),
|
||||
name: (json['name'] ?? '').toString(),
|
||||
url: url,
|
||||
enabled: json['enabled'] is bool ? json['enabled'] as bool : true,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
other is DanmakuSource &&
|
||||
id == other.id &&
|
||||
name == other.name &&
|
||||
url == other.url &&
|
||||
enabled == other.enabled;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(id, name, url, enabled);
|
||||
}
|
||||
|
||||
|
||||
enum DanmakuCommentStatus { loading, ready, failed }
|
||||
|
||||
|
||||
sealed class DanmakuSessionState {
|
||||
const DanmakuSessionState();
|
||||
}
|
||||
|
||||
|
||||
class DanmakuSessionIdle extends DanmakuSessionState {
|
||||
const DanmakuSessionIdle();
|
||||
}
|
||||
|
||||
|
||||
class DanmakuSessionMatching extends DanmakuSessionState {
|
||||
const DanmakuSessionMatching();
|
||||
}
|
||||
|
||||
|
||||
class DanmakuSessionMatched extends DanmakuSessionState {
|
||||
final List<DanmakuMatchCandidate> matches;
|
||||
final int selectedIndex;
|
||||
final DanmakuCommentStatus commentStatus;
|
||||
final List<DanmakuComment> comments;
|
||||
|
||||
const DanmakuSessionMatched({
|
||||
required this.matches,
|
||||
required this.selectedIndex,
|
||||
required this.commentStatus,
|
||||
this.comments = const [],
|
||||
});
|
||||
|
||||
DanmakuSessionMatched copyWith({
|
||||
List<DanmakuMatchCandidate>? matches,
|
||||
int? selectedIndex,
|
||||
DanmakuCommentStatus? commentStatus,
|
||||
List<DanmakuComment>? comments,
|
||||
}) => DanmakuSessionMatched(
|
||||
matches: matches ?? this.matches,
|
||||
selectedIndex: selectedIndex ?? this.selectedIndex,
|
||||
commentStatus: commentStatus ?? this.commentStatus,
|
||||
comments: comments ?? this.comments,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
class DanmakuSessionEmpty extends DanmakuSessionState {
|
||||
const DanmakuSessionEmpty();
|
||||
}
|
||||
|
||||
|
||||
class DanmakuSessionFailed extends DanmakuSessionState {
|
||||
final String message;
|
||||
const DanmakuSessionFailed(this.message);
|
||||
}
|
||||
|
||||
|
||||
@freezed
|
||||
abstract class DanmakuMatchContext with _$DanmakuMatchContext {
|
||||
const factory DanmakuMatchContext({
|
||||
required String itemId,
|
||||
required String name,
|
||||
required String? type,
|
||||
required String? seriesName,
|
||||
required int? season,
|
||||
required int? episode,
|
||||
required int durationSec,
|
||||
}) = _DanmakuMatchContext;
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class DanmakuMatch with _$DanmakuMatch {
|
||||
const factory DanmakuMatch({
|
||||
@JsonKey(fromJson: _intRequired) required int episodeId,
|
||||
@JsonKey(fromJson: _intOrZero) @Default(0) int animeId,
|
||||
@Default('') String animeTitle,
|
||||
@Default('') String episodeTitle,
|
||||
}) = _DanmakuMatch;
|
||||
|
||||
factory DanmakuMatch.fromJson(Map<String, dynamic> json) =>
|
||||
_$DanmakuMatchFromJson(json);
|
||||
}
|
||||
|
||||
class DanmakuMatchCandidate {
|
||||
final DanmakuSource source;
|
||||
final DanmakuMatch match;
|
||||
|
||||
const DanmakuMatchCandidate({required this.source, required this.match});
|
||||
|
||||
int get episodeId => match.episodeId;
|
||||
int get animeId => match.animeId;
|
||||
String get animeTitle => match.animeTitle;
|
||||
String get episodeTitle => match.episodeTitle;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
other is DanmakuMatchCandidate &&
|
||||
source == other.source &&
|
||||
match == other.match;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(source, match);
|
||||
}
|
||||
|
||||
|
||||
@freezed
|
||||
abstract class DanmakuBangumiEpisode with _$DanmakuBangumiEpisode {
|
||||
const factory DanmakuBangumiEpisode({
|
||||
@JsonKey(fromJson: _intRequired) required int episodeId,
|
||||
@Default('') String episodeTitle,
|
||||
@JsonKey(fromJson: _stringFromAny) @Default('') String episodeNumber,
|
||||
}) = _DanmakuBangumiEpisode;
|
||||
|
||||
factory DanmakuBangumiEpisode.fromJson(Map<String, dynamic> json) =>
|
||||
_$DanmakuBangumiEpisodeFromJson(json);
|
||||
}
|
||||
|
||||
int _intRequired(Object? value) => (value as num).toInt();
|
||||
|
||||
int _intOrZero(Object? value) => (value as num?)?.toInt() ?? 0;
|
||||
|
||||
String _stringFromAny(Object? value) => value?.toString() ?? '';
|
||||
|
||||
class DanmakuComment {
|
||||
final int cid;
|
||||
final double time;
|
||||
final int type;
|
||||
final Color color;
|
||||
final String text;
|
||||
int lane;
|
||||
|
||||
DanmakuComment({
|
||||
required this.cid,
|
||||
required this.time,
|
||||
required this.type,
|
||||
required this.color,
|
||||
required this.text,
|
||||
this.lane = 0,
|
||||
});
|
||||
|
||||
|
||||
factory DanmakuComment.fromRaw(int cid, String p, String m) {
|
||||
final parts = p.split(',');
|
||||
final time = parts.isNotEmpty ? (double.tryParse(parts[0]) ?? 0.0) : 0.0;
|
||||
final type = parts.length > 1 ? (int.tryParse(parts[1]) ?? 1) : 1;
|
||||
final colorInt = parts.length > 2
|
||||
? (int.tryParse(parts[2]) ?? 0xFFFFFF)
|
||||
: 0xFFFFFF;
|
||||
return DanmakuComment(
|
||||
cid: cid,
|
||||
time: time,
|
||||
type: type,
|
||||
color: Color(0xFF000000 | (colorInt & 0xFFFFFF)),
|
||||
text: m,
|
||||
);
|
||||
}
|
||||
}
|
||||
Generated
+830
@@ -0,0 +1,830 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'danmaku.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$DanmakuMatchContext {
|
||||
|
||||
String get itemId; String get name; String? get type; String? get seriesName; int? get season; int? get episode; int get durationSec;
|
||||
/// Create a copy of DanmakuMatchContext
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$DanmakuMatchContextCopyWith<DanmakuMatchContext> get copyWith => _$DanmakuMatchContextCopyWithImpl<DanmakuMatchContext>(this as DanmakuMatchContext, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is DanmakuMatchContext&&(identical(other.itemId, itemId) || other.itemId == itemId)&&(identical(other.name, name) || other.name == name)&&(identical(other.type, type) || other.type == type)&&(identical(other.seriesName, seriesName) || other.seriesName == seriesName)&&(identical(other.season, season) || other.season == season)&&(identical(other.episode, episode) || other.episode == episode)&&(identical(other.durationSec, durationSec) || other.durationSec == durationSec));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,itemId,name,type,seriesName,season,episode,durationSec);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'DanmakuMatchContext(itemId: $itemId, name: $name, type: $type, seriesName: $seriesName, season: $season, episode: $episode, durationSec: $durationSec)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $DanmakuMatchContextCopyWith<$Res> {
|
||||
factory $DanmakuMatchContextCopyWith(DanmakuMatchContext value, $Res Function(DanmakuMatchContext) _then) = _$DanmakuMatchContextCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String itemId, String name, String? type, String? seriesName, int? season, int? episode, int durationSec
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$DanmakuMatchContextCopyWithImpl<$Res>
|
||||
implements $DanmakuMatchContextCopyWith<$Res> {
|
||||
_$DanmakuMatchContextCopyWithImpl(this._self, this._then);
|
||||
|
||||
final DanmakuMatchContext _self;
|
||||
final $Res Function(DanmakuMatchContext) _then;
|
||||
|
||||
/// Create a copy of DanmakuMatchContext
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? itemId = null,Object? name = null,Object? type = freezed,Object? seriesName = freezed,Object? season = freezed,Object? episode = freezed,Object? durationSec = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
itemId: null == itemId ? _self.itemId : itemId // ignore: cast_nullable_to_non_nullable
|
||||
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
as String,type: freezed == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||
as String?,seriesName: freezed == seriesName ? _self.seriesName : seriesName // ignore: cast_nullable_to_non_nullable
|
||||
as String?,season: freezed == season ? _self.season : season // ignore: cast_nullable_to_non_nullable
|
||||
as int?,episode: freezed == episode ? _self.episode : episode // ignore: cast_nullable_to_non_nullable
|
||||
as int?,durationSec: null == durationSec ? _self.durationSec : durationSec // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [DanmakuMatchContext].
|
||||
extension DanmakuMatchContextPatterns on DanmakuMatchContext {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _DanmakuMatchContext value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _DanmakuMatchContext() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _DanmakuMatchContext value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _DanmakuMatchContext():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _DanmakuMatchContext value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _DanmakuMatchContext() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String itemId, String name, String? type, String? seriesName, int? season, int? episode, int durationSec)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _DanmakuMatchContext() when $default != null:
|
||||
return $default(_that.itemId,_that.name,_that.type,_that.seriesName,_that.season,_that.episode,_that.durationSec);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String itemId, String name, String? type, String? seriesName, int? season, int? episode, int durationSec) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _DanmakuMatchContext():
|
||||
return $default(_that.itemId,_that.name,_that.type,_that.seriesName,_that.season,_that.episode,_that.durationSec);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String itemId, String name, String? type, String? seriesName, int? season, int? episode, int durationSec)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _DanmakuMatchContext() when $default != null:
|
||||
return $default(_that.itemId,_that.name,_that.type,_that.seriesName,_that.season,_that.episode,_that.durationSec);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class _DanmakuMatchContext implements DanmakuMatchContext {
|
||||
const _DanmakuMatchContext({required this.itemId, required this.name, required this.type, required this.seriesName, required this.season, required this.episode, required this.durationSec});
|
||||
|
||||
|
||||
@override final String itemId;
|
||||
@override final String name;
|
||||
@override final String? type;
|
||||
@override final String? seriesName;
|
||||
@override final int? season;
|
||||
@override final int? episode;
|
||||
@override final int durationSec;
|
||||
|
||||
/// Create a copy of DanmakuMatchContext
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$DanmakuMatchContextCopyWith<_DanmakuMatchContext> get copyWith => __$DanmakuMatchContextCopyWithImpl<_DanmakuMatchContext>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _DanmakuMatchContext&&(identical(other.itemId, itemId) || other.itemId == itemId)&&(identical(other.name, name) || other.name == name)&&(identical(other.type, type) || other.type == type)&&(identical(other.seriesName, seriesName) || other.seriesName == seriesName)&&(identical(other.season, season) || other.season == season)&&(identical(other.episode, episode) || other.episode == episode)&&(identical(other.durationSec, durationSec) || other.durationSec == durationSec));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,itemId,name,type,seriesName,season,episode,durationSec);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'DanmakuMatchContext(itemId: $itemId, name: $name, type: $type, seriesName: $seriesName, season: $season, episode: $episode, durationSec: $durationSec)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$DanmakuMatchContextCopyWith<$Res> implements $DanmakuMatchContextCopyWith<$Res> {
|
||||
factory _$DanmakuMatchContextCopyWith(_DanmakuMatchContext value, $Res Function(_DanmakuMatchContext) _then) = __$DanmakuMatchContextCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String itemId, String name, String? type, String? seriesName, int? season, int? episode, int durationSec
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$DanmakuMatchContextCopyWithImpl<$Res>
|
||||
implements _$DanmakuMatchContextCopyWith<$Res> {
|
||||
__$DanmakuMatchContextCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _DanmakuMatchContext _self;
|
||||
final $Res Function(_DanmakuMatchContext) _then;
|
||||
|
||||
/// Create a copy of DanmakuMatchContext
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? itemId = null,Object? name = null,Object? type = freezed,Object? seriesName = freezed,Object? season = freezed,Object? episode = freezed,Object? durationSec = null,}) {
|
||||
return _then(_DanmakuMatchContext(
|
||||
itemId: null == itemId ? _self.itemId : itemId // ignore: cast_nullable_to_non_nullable
|
||||
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
as String,type: freezed == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||
as String?,seriesName: freezed == seriesName ? _self.seriesName : seriesName // ignore: cast_nullable_to_non_nullable
|
||||
as String?,season: freezed == season ? _self.season : season // ignore: cast_nullable_to_non_nullable
|
||||
as int?,episode: freezed == episode ? _self.episode : episode // ignore: cast_nullable_to_non_nullable
|
||||
as int?,durationSec: null == durationSec ? _self.durationSec : durationSec // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// @nodoc
|
||||
mixin _$DanmakuMatch {
|
||||
|
||||
@JsonKey(fromJson: _intRequired) int get episodeId;@JsonKey(fromJson: _intOrZero) int get animeId; String get animeTitle; String get episodeTitle;
|
||||
/// Create a copy of DanmakuMatch
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$DanmakuMatchCopyWith<DanmakuMatch> get copyWith => _$DanmakuMatchCopyWithImpl<DanmakuMatch>(this as DanmakuMatch, _$identity);
|
||||
|
||||
/// Serializes this DanmakuMatch to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is DanmakuMatch&&(identical(other.episodeId, episodeId) || other.episodeId == episodeId)&&(identical(other.animeId, animeId) || other.animeId == animeId)&&(identical(other.animeTitle, animeTitle) || other.animeTitle == animeTitle)&&(identical(other.episodeTitle, episodeTitle) || other.episodeTitle == episodeTitle));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,episodeId,animeId,animeTitle,episodeTitle);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'DanmakuMatch(episodeId: $episodeId, animeId: $animeId, animeTitle: $animeTitle, episodeTitle: $episodeTitle)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $DanmakuMatchCopyWith<$Res> {
|
||||
factory $DanmakuMatchCopyWith(DanmakuMatch value, $Res Function(DanmakuMatch) _then) = _$DanmakuMatchCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
@JsonKey(fromJson: _intRequired) int episodeId,@JsonKey(fromJson: _intOrZero) int animeId, String animeTitle, String episodeTitle
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$DanmakuMatchCopyWithImpl<$Res>
|
||||
implements $DanmakuMatchCopyWith<$Res> {
|
||||
_$DanmakuMatchCopyWithImpl(this._self, this._then);
|
||||
|
||||
final DanmakuMatch _self;
|
||||
final $Res Function(DanmakuMatch) _then;
|
||||
|
||||
/// Create a copy of DanmakuMatch
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? episodeId = null,Object? animeId = null,Object? animeTitle = null,Object? episodeTitle = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
episodeId: null == episodeId ? _self.episodeId : episodeId // ignore: cast_nullable_to_non_nullable
|
||||
as int,animeId: null == animeId ? _self.animeId : animeId // ignore: cast_nullable_to_non_nullable
|
||||
as int,animeTitle: null == animeTitle ? _self.animeTitle : animeTitle // ignore: cast_nullable_to_non_nullable
|
||||
as String,episodeTitle: null == episodeTitle ? _self.episodeTitle : episodeTitle // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [DanmakuMatch].
|
||||
extension DanmakuMatchPatterns on DanmakuMatch {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _DanmakuMatch value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _DanmakuMatch() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _DanmakuMatch value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _DanmakuMatch():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _DanmakuMatch value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _DanmakuMatch() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function(@JsonKey(fromJson: _intRequired) int episodeId, @JsonKey(fromJson: _intOrZero) int animeId, String animeTitle, String episodeTitle)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _DanmakuMatch() when $default != null:
|
||||
return $default(_that.episodeId,_that.animeId,_that.animeTitle,_that.episodeTitle);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function(@JsonKey(fromJson: _intRequired) int episodeId, @JsonKey(fromJson: _intOrZero) int animeId, String animeTitle, String episodeTitle) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _DanmakuMatch():
|
||||
return $default(_that.episodeId,_that.animeId,_that.animeTitle,_that.episodeTitle);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function(@JsonKey(fromJson: _intRequired) int episodeId, @JsonKey(fromJson: _intOrZero) int animeId, String animeTitle, String episodeTitle)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _DanmakuMatch() when $default != null:
|
||||
return $default(_that.episodeId,_that.animeId,_that.animeTitle,_that.episodeTitle);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _DanmakuMatch implements DanmakuMatch {
|
||||
const _DanmakuMatch({@JsonKey(fromJson: _intRequired) required this.episodeId, @JsonKey(fromJson: _intOrZero) this.animeId = 0, this.animeTitle = '', this.episodeTitle = ''});
|
||||
factory _DanmakuMatch.fromJson(Map<String, dynamic> json) => _$DanmakuMatchFromJson(json);
|
||||
|
||||
@override@JsonKey(fromJson: _intRequired) final int episodeId;
|
||||
@override@JsonKey(fromJson: _intOrZero) final int animeId;
|
||||
@override@JsonKey() final String animeTitle;
|
||||
@override@JsonKey() final String episodeTitle;
|
||||
|
||||
/// Create a copy of DanmakuMatch
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$DanmakuMatchCopyWith<_DanmakuMatch> get copyWith => __$DanmakuMatchCopyWithImpl<_DanmakuMatch>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$DanmakuMatchToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _DanmakuMatch&&(identical(other.episodeId, episodeId) || other.episodeId == episodeId)&&(identical(other.animeId, animeId) || other.animeId == animeId)&&(identical(other.animeTitle, animeTitle) || other.animeTitle == animeTitle)&&(identical(other.episodeTitle, episodeTitle) || other.episodeTitle == episodeTitle));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,episodeId,animeId,animeTitle,episodeTitle);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'DanmakuMatch(episodeId: $episodeId, animeId: $animeId, animeTitle: $animeTitle, episodeTitle: $episodeTitle)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$DanmakuMatchCopyWith<$Res> implements $DanmakuMatchCopyWith<$Res> {
|
||||
factory _$DanmakuMatchCopyWith(_DanmakuMatch value, $Res Function(_DanmakuMatch) _then) = __$DanmakuMatchCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
@JsonKey(fromJson: _intRequired) int episodeId,@JsonKey(fromJson: _intOrZero) int animeId, String animeTitle, String episodeTitle
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$DanmakuMatchCopyWithImpl<$Res>
|
||||
implements _$DanmakuMatchCopyWith<$Res> {
|
||||
__$DanmakuMatchCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _DanmakuMatch _self;
|
||||
final $Res Function(_DanmakuMatch) _then;
|
||||
|
||||
/// Create a copy of DanmakuMatch
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? episodeId = null,Object? animeId = null,Object? animeTitle = null,Object? episodeTitle = null,}) {
|
||||
return _then(_DanmakuMatch(
|
||||
episodeId: null == episodeId ? _self.episodeId : episodeId // ignore: cast_nullable_to_non_nullable
|
||||
as int,animeId: null == animeId ? _self.animeId : animeId // ignore: cast_nullable_to_non_nullable
|
||||
as int,animeTitle: null == animeTitle ? _self.animeTitle : animeTitle // ignore: cast_nullable_to_non_nullable
|
||||
as String,episodeTitle: null == episodeTitle ? _self.episodeTitle : episodeTitle // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// @nodoc
|
||||
mixin _$DanmakuBangumiEpisode {
|
||||
|
||||
@JsonKey(fromJson: _intRequired) int get episodeId; String get episodeTitle;@JsonKey(fromJson: _stringFromAny) String get episodeNumber;
|
||||
/// Create a copy of DanmakuBangumiEpisode
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$DanmakuBangumiEpisodeCopyWith<DanmakuBangumiEpisode> get copyWith => _$DanmakuBangumiEpisodeCopyWithImpl<DanmakuBangumiEpisode>(this as DanmakuBangumiEpisode, _$identity);
|
||||
|
||||
/// Serializes this DanmakuBangumiEpisode to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is DanmakuBangumiEpisode&&(identical(other.episodeId, episodeId) || other.episodeId == episodeId)&&(identical(other.episodeTitle, episodeTitle) || other.episodeTitle == episodeTitle)&&(identical(other.episodeNumber, episodeNumber) || other.episodeNumber == episodeNumber));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,episodeId,episodeTitle,episodeNumber);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'DanmakuBangumiEpisode(episodeId: $episodeId, episodeTitle: $episodeTitle, episodeNumber: $episodeNumber)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $DanmakuBangumiEpisodeCopyWith<$Res> {
|
||||
factory $DanmakuBangumiEpisodeCopyWith(DanmakuBangumiEpisode value, $Res Function(DanmakuBangumiEpisode) _then) = _$DanmakuBangumiEpisodeCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
@JsonKey(fromJson: _intRequired) int episodeId, String episodeTitle,@JsonKey(fromJson: _stringFromAny) String episodeNumber
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$DanmakuBangumiEpisodeCopyWithImpl<$Res>
|
||||
implements $DanmakuBangumiEpisodeCopyWith<$Res> {
|
||||
_$DanmakuBangumiEpisodeCopyWithImpl(this._self, this._then);
|
||||
|
||||
final DanmakuBangumiEpisode _self;
|
||||
final $Res Function(DanmakuBangumiEpisode) _then;
|
||||
|
||||
/// Create a copy of DanmakuBangumiEpisode
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? episodeId = null,Object? episodeTitle = null,Object? episodeNumber = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
episodeId: null == episodeId ? _self.episodeId : episodeId // ignore: cast_nullable_to_non_nullable
|
||||
as int,episodeTitle: null == episodeTitle ? _self.episodeTitle : episodeTitle // ignore: cast_nullable_to_non_nullable
|
||||
as String,episodeNumber: null == episodeNumber ? _self.episodeNumber : episodeNumber // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [DanmakuBangumiEpisode].
|
||||
extension DanmakuBangumiEpisodePatterns on DanmakuBangumiEpisode {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _DanmakuBangumiEpisode value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _DanmakuBangumiEpisode() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _DanmakuBangumiEpisode value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _DanmakuBangumiEpisode():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _DanmakuBangumiEpisode value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _DanmakuBangumiEpisode() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function(@JsonKey(fromJson: _intRequired) int episodeId, String episodeTitle, @JsonKey(fromJson: _stringFromAny) String episodeNumber)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _DanmakuBangumiEpisode() when $default != null:
|
||||
return $default(_that.episodeId,_that.episodeTitle,_that.episodeNumber);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function(@JsonKey(fromJson: _intRequired) int episodeId, String episodeTitle, @JsonKey(fromJson: _stringFromAny) String episodeNumber) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _DanmakuBangumiEpisode():
|
||||
return $default(_that.episodeId,_that.episodeTitle,_that.episodeNumber);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function(@JsonKey(fromJson: _intRequired) int episodeId, String episodeTitle, @JsonKey(fromJson: _stringFromAny) String episodeNumber)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _DanmakuBangumiEpisode() when $default != null:
|
||||
return $default(_that.episodeId,_that.episodeTitle,_that.episodeNumber);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _DanmakuBangumiEpisode implements DanmakuBangumiEpisode {
|
||||
const _DanmakuBangumiEpisode({@JsonKey(fromJson: _intRequired) required this.episodeId, this.episodeTitle = '', @JsonKey(fromJson: _stringFromAny) this.episodeNumber = ''});
|
||||
factory _DanmakuBangumiEpisode.fromJson(Map<String, dynamic> json) => _$DanmakuBangumiEpisodeFromJson(json);
|
||||
|
||||
@override@JsonKey(fromJson: _intRequired) final int episodeId;
|
||||
@override@JsonKey() final String episodeTitle;
|
||||
@override@JsonKey(fromJson: _stringFromAny) final String episodeNumber;
|
||||
|
||||
/// Create a copy of DanmakuBangumiEpisode
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$DanmakuBangumiEpisodeCopyWith<_DanmakuBangumiEpisode> get copyWith => __$DanmakuBangumiEpisodeCopyWithImpl<_DanmakuBangumiEpisode>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$DanmakuBangumiEpisodeToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _DanmakuBangumiEpisode&&(identical(other.episodeId, episodeId) || other.episodeId == episodeId)&&(identical(other.episodeTitle, episodeTitle) || other.episodeTitle == episodeTitle)&&(identical(other.episodeNumber, episodeNumber) || other.episodeNumber == episodeNumber));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,episodeId,episodeTitle,episodeNumber);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'DanmakuBangumiEpisode(episodeId: $episodeId, episodeTitle: $episodeTitle, episodeNumber: $episodeNumber)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$DanmakuBangumiEpisodeCopyWith<$Res> implements $DanmakuBangumiEpisodeCopyWith<$Res> {
|
||||
factory _$DanmakuBangumiEpisodeCopyWith(_DanmakuBangumiEpisode value, $Res Function(_DanmakuBangumiEpisode) _then) = __$DanmakuBangumiEpisodeCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
@JsonKey(fromJson: _intRequired) int episodeId, String episodeTitle,@JsonKey(fromJson: _stringFromAny) String episodeNumber
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$DanmakuBangumiEpisodeCopyWithImpl<$Res>
|
||||
implements _$DanmakuBangumiEpisodeCopyWith<$Res> {
|
||||
__$DanmakuBangumiEpisodeCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _DanmakuBangumiEpisode _self;
|
||||
final $Res Function(_DanmakuBangumiEpisode) _then;
|
||||
|
||||
/// Create a copy of DanmakuBangumiEpisode
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? episodeId = null,Object? episodeTitle = null,Object? episodeNumber = null,}) {
|
||||
return _then(_DanmakuBangumiEpisode(
|
||||
episodeId: null == episodeId ? _self.episodeId : episodeId // ignore: cast_nullable_to_non_nullable
|
||||
as int,episodeTitle: null == episodeTitle ? _self.episodeTitle : episodeTitle // ignore: cast_nullable_to_non_nullable
|
||||
as String,episodeNumber: null == episodeNumber ? _self.episodeNumber : episodeNumber // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
Generated
+41
@@ -0,0 +1,41 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'danmaku.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_DanmakuMatch _$DanmakuMatchFromJson(Map<String, dynamic> json) =>
|
||||
_DanmakuMatch(
|
||||
episodeId: _intRequired(json['episodeId']),
|
||||
animeId: json['animeId'] == null ? 0 : _intOrZero(json['animeId']),
|
||||
animeTitle: json['animeTitle'] as String? ?? '',
|
||||
episodeTitle: json['episodeTitle'] as String? ?? '',
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$DanmakuMatchToJson(_DanmakuMatch instance) =>
|
||||
<String, dynamic>{
|
||||
'episodeId': instance.episodeId,
|
||||
'animeId': instance.animeId,
|
||||
'animeTitle': instance.animeTitle,
|
||||
'episodeTitle': instance.episodeTitle,
|
||||
};
|
||||
|
||||
_DanmakuBangumiEpisode _$DanmakuBangumiEpisodeFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => _DanmakuBangumiEpisode(
|
||||
episodeId: _intRequired(json['episodeId']),
|
||||
episodeTitle: json['episodeTitle'] as String? ?? '',
|
||||
episodeNumber: json['episodeNumber'] == null
|
||||
? ''
|
||||
: _stringFromAny(json['episodeNumber']),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$DanmakuBangumiEpisodeToJson(
|
||||
_DanmakuBangumiEpisode instance,
|
||||
) => <String, dynamic>{
|
||||
'episodeId': instance.episodeId,
|
||||
'episodeTitle': instance.episodeTitle,
|
||||
'episodeNumber': instance.episodeNumber,
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
enum ErrorCode {
|
||||
invalidParams('INVALID_PARAMS'),
|
||||
serverUnreachable('SERVER_UNREACHABLE'),
|
||||
serverTimeout('SERVER_TIMEOUT'),
|
||||
serverHttpError('SERVER_HTTP_ERROR'),
|
||||
authInvalidCredentials('AUTH_INVALID_CREDENTIALS'),
|
||||
authForbidden('AUTH_FORBIDDEN'),
|
||||
storeConflict('STORE_CONFLICT'),
|
||||
storeNotFound('STORE_NOT_FOUND'),
|
||||
internalError('INTERNAL_ERROR');
|
||||
|
||||
final String value;
|
||||
const ErrorCode(this.value);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'icon_library.freezed.dart';
|
||||
part 'icon_library.g.dart';
|
||||
|
||||
@freezed
|
||||
abstract class IconEntry with _$IconEntry {
|
||||
const factory IconEntry({
|
||||
@JsonKey(fromJson: _stringOrEmpty) @Default('') String name,
|
||||
@JsonKey(fromJson: _stringOrEmpty) @Default('') String url,
|
||||
}) = _IconEntry;
|
||||
|
||||
factory IconEntry.fromJson(Map<String, dynamic> json) =>
|
||||
_$IconEntryFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class IconLibrary with _$IconLibrary {
|
||||
const factory IconLibrary({
|
||||
|
||||
required String sourceUrl,
|
||||
@JsonKey(fromJson: _stringOrEmpty) @Default('') String name,
|
||||
@JsonKey(fromJson: _stringOrEmpty) @Default('') String description,
|
||||
@Default(<IconEntry>[]) List<IconEntry> icons,
|
||||
}) = _IconLibrary;
|
||||
|
||||
factory IconLibrary.fromJson(Map<String, dynamic> json) =>
|
||||
_$IconLibraryFromJson(json);
|
||||
|
||||
|
||||
static IconLibrary parse(String sourceUrl, Map<String, dynamic> json) {
|
||||
final raw = json['icons'];
|
||||
final iconsList = raw is List
|
||||
? raw
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(IconEntry.fromJson)
|
||||
.where((e) => e.url.isNotEmpty)
|
||||
.toList()
|
||||
: const <IconEntry>[];
|
||||
return IconLibrary(
|
||||
sourceUrl: sourceUrl,
|
||||
name: (json['name'] as String?) ?? '',
|
||||
description: (json['description'] as String?) ?? '',
|
||||
icons: iconsList,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _stringOrEmpty(Object? value) => value?.toString() ?? '';
|
||||
+560
@@ -0,0 +1,560 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'icon_library.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$IconEntry {
|
||||
|
||||
@JsonKey(fromJson: _stringOrEmpty) String get name;@JsonKey(fromJson: _stringOrEmpty) String get url;
|
||||
/// Create a copy of IconEntry
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$IconEntryCopyWith<IconEntry> get copyWith => _$IconEntryCopyWithImpl<IconEntry>(this as IconEntry, _$identity);
|
||||
|
||||
/// Serializes this IconEntry to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is IconEntry&&(identical(other.name, name) || other.name == name)&&(identical(other.url, url) || other.url == url));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,name,url);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'IconEntry(name: $name, url: $url)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $IconEntryCopyWith<$Res> {
|
||||
factory $IconEntryCopyWith(IconEntry value, $Res Function(IconEntry) _then) = _$IconEntryCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
@JsonKey(fromJson: _stringOrEmpty) String name,@JsonKey(fromJson: _stringOrEmpty) String url
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$IconEntryCopyWithImpl<$Res>
|
||||
implements $IconEntryCopyWith<$Res> {
|
||||
_$IconEntryCopyWithImpl(this._self, this._then);
|
||||
|
||||
final IconEntry _self;
|
||||
final $Res Function(IconEntry) _then;
|
||||
|
||||
/// Create a copy of IconEntry
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? url = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [IconEntry].
|
||||
extension IconEntryPatterns on IconEntry {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _IconEntry value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _IconEntry() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _IconEntry value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _IconEntry():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _IconEntry value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _IconEntry() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function(@JsonKey(fromJson: _stringOrEmpty) String name, @JsonKey(fromJson: _stringOrEmpty) String url)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _IconEntry() when $default != null:
|
||||
return $default(_that.name,_that.url);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function(@JsonKey(fromJson: _stringOrEmpty) String name, @JsonKey(fromJson: _stringOrEmpty) String url) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _IconEntry():
|
||||
return $default(_that.name,_that.url);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function(@JsonKey(fromJson: _stringOrEmpty) String name, @JsonKey(fromJson: _stringOrEmpty) String url)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _IconEntry() when $default != null:
|
||||
return $default(_that.name,_that.url);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _IconEntry implements IconEntry {
|
||||
const _IconEntry({@JsonKey(fromJson: _stringOrEmpty) this.name = '', @JsonKey(fromJson: _stringOrEmpty) this.url = ''});
|
||||
factory _IconEntry.fromJson(Map<String, dynamic> json) => _$IconEntryFromJson(json);
|
||||
|
||||
@override@JsonKey(fromJson: _stringOrEmpty) final String name;
|
||||
@override@JsonKey(fromJson: _stringOrEmpty) final String url;
|
||||
|
||||
/// Create a copy of IconEntry
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$IconEntryCopyWith<_IconEntry> get copyWith => __$IconEntryCopyWithImpl<_IconEntry>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$IconEntryToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _IconEntry&&(identical(other.name, name) || other.name == name)&&(identical(other.url, url) || other.url == url));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,name,url);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'IconEntry(name: $name, url: $url)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$IconEntryCopyWith<$Res> implements $IconEntryCopyWith<$Res> {
|
||||
factory _$IconEntryCopyWith(_IconEntry value, $Res Function(_IconEntry) _then) = __$IconEntryCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
@JsonKey(fromJson: _stringOrEmpty) String name,@JsonKey(fromJson: _stringOrEmpty) String url
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$IconEntryCopyWithImpl<$Res>
|
||||
implements _$IconEntryCopyWith<$Res> {
|
||||
__$IconEntryCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _IconEntry _self;
|
||||
final $Res Function(_IconEntry) _then;
|
||||
|
||||
/// Create a copy of IconEntry
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? name = null,Object? url = null,}) {
|
||||
return _then(_IconEntry(
|
||||
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// @nodoc
|
||||
mixin _$IconLibrary {
|
||||
|
||||
/// 来源 URL(去除尾随斜杠后的标准化字符串)
|
||||
String get sourceUrl;@JsonKey(fromJson: _stringOrEmpty) String get name;@JsonKey(fromJson: _stringOrEmpty) String get description; List<IconEntry> get icons;
|
||||
/// Create a copy of IconLibrary
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$IconLibraryCopyWith<IconLibrary> get copyWith => _$IconLibraryCopyWithImpl<IconLibrary>(this as IconLibrary, _$identity);
|
||||
|
||||
/// Serializes this IconLibrary to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is IconLibrary&&(identical(other.sourceUrl, sourceUrl) || other.sourceUrl == sourceUrl)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&const DeepCollectionEquality().equals(other.icons, icons));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,sourceUrl,name,description,const DeepCollectionEquality().hash(icons));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'IconLibrary(sourceUrl: $sourceUrl, name: $name, description: $description, icons: $icons)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $IconLibraryCopyWith<$Res> {
|
||||
factory $IconLibraryCopyWith(IconLibrary value, $Res Function(IconLibrary) _then) = _$IconLibraryCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String sourceUrl,@JsonKey(fromJson: _stringOrEmpty) String name,@JsonKey(fromJson: _stringOrEmpty) String description, List<IconEntry> icons
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$IconLibraryCopyWithImpl<$Res>
|
||||
implements $IconLibraryCopyWith<$Res> {
|
||||
_$IconLibraryCopyWithImpl(this._self, this._then);
|
||||
|
||||
final IconLibrary _self;
|
||||
final $Res Function(IconLibrary) _then;
|
||||
|
||||
/// Create a copy of IconLibrary
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? sourceUrl = null,Object? name = null,Object? description = null,Object? icons = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
sourceUrl: null == sourceUrl ? _self.sourceUrl : sourceUrl // ignore: cast_nullable_to_non_nullable
|
||||
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
as String,description: null == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
|
||||
as String,icons: null == icons ? _self.icons : icons // ignore: cast_nullable_to_non_nullable
|
||||
as List<IconEntry>,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [IconLibrary].
|
||||
extension IconLibraryPatterns on IconLibrary {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _IconLibrary value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _IconLibrary() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _IconLibrary value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _IconLibrary():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _IconLibrary value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _IconLibrary() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String sourceUrl, @JsonKey(fromJson: _stringOrEmpty) String name, @JsonKey(fromJson: _stringOrEmpty) String description, List<IconEntry> icons)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _IconLibrary() when $default != null:
|
||||
return $default(_that.sourceUrl,_that.name,_that.description,_that.icons);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String sourceUrl, @JsonKey(fromJson: _stringOrEmpty) String name, @JsonKey(fromJson: _stringOrEmpty) String description, List<IconEntry> icons) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _IconLibrary():
|
||||
return $default(_that.sourceUrl,_that.name,_that.description,_that.icons);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String sourceUrl, @JsonKey(fromJson: _stringOrEmpty) String name, @JsonKey(fromJson: _stringOrEmpty) String description, List<IconEntry> icons)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _IconLibrary() when $default != null:
|
||||
return $default(_that.sourceUrl,_that.name,_that.description,_that.icons);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _IconLibrary implements IconLibrary {
|
||||
const _IconLibrary({required this.sourceUrl, @JsonKey(fromJson: _stringOrEmpty) this.name = '', @JsonKey(fromJson: _stringOrEmpty) this.description = '', final List<IconEntry> icons = const <IconEntry>[]}): _icons = icons;
|
||||
factory _IconLibrary.fromJson(Map<String, dynamic> json) => _$IconLibraryFromJson(json);
|
||||
|
||||
/// 来源 URL(去除尾随斜杠后的标准化字符串)
|
||||
@override final String sourceUrl;
|
||||
@override@JsonKey(fromJson: _stringOrEmpty) final String name;
|
||||
@override@JsonKey(fromJson: _stringOrEmpty) final String description;
|
||||
final List<IconEntry> _icons;
|
||||
@override@JsonKey() List<IconEntry> get icons {
|
||||
if (_icons is EqualUnmodifiableListView) return _icons;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_icons);
|
||||
}
|
||||
|
||||
|
||||
/// Create a copy of IconLibrary
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$IconLibraryCopyWith<_IconLibrary> get copyWith => __$IconLibraryCopyWithImpl<_IconLibrary>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$IconLibraryToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _IconLibrary&&(identical(other.sourceUrl, sourceUrl) || other.sourceUrl == sourceUrl)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&const DeepCollectionEquality().equals(other._icons, _icons));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,sourceUrl,name,description,const DeepCollectionEquality().hash(_icons));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'IconLibrary(sourceUrl: $sourceUrl, name: $name, description: $description, icons: $icons)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$IconLibraryCopyWith<$Res> implements $IconLibraryCopyWith<$Res> {
|
||||
factory _$IconLibraryCopyWith(_IconLibrary value, $Res Function(_IconLibrary) _then) = __$IconLibraryCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String sourceUrl,@JsonKey(fromJson: _stringOrEmpty) String name,@JsonKey(fromJson: _stringOrEmpty) String description, List<IconEntry> icons
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$IconLibraryCopyWithImpl<$Res>
|
||||
implements _$IconLibraryCopyWith<$Res> {
|
||||
__$IconLibraryCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _IconLibrary _self;
|
||||
final $Res Function(_IconLibrary) _then;
|
||||
|
||||
/// Create a copy of IconLibrary
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? sourceUrl = null,Object? name = null,Object? description = null,Object? icons = null,}) {
|
||||
return _then(_IconLibrary(
|
||||
sourceUrl: null == sourceUrl ? _self.sourceUrl : sourceUrl // ignore: cast_nullable_to_non_nullable
|
||||
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
as String,description: null == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
|
||||
as String,icons: null == icons ? _self._icons : icons // ignore: cast_nullable_to_non_nullable
|
||||
as List<IconEntry>,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
Generated
+36
@@ -0,0 +1,36 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'icon_library.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_IconEntry _$IconEntryFromJson(Map<String, dynamic> json) => _IconEntry(
|
||||
name: json['name'] == null ? '' : _stringOrEmpty(json['name']),
|
||||
url: json['url'] == null ? '' : _stringOrEmpty(json['url']),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$IconEntryToJson(_IconEntry instance) =>
|
||||
<String, dynamic>{'name': instance.name, 'url': instance.url};
|
||||
|
||||
_IconLibrary _$IconLibraryFromJson(Map<String, dynamic> json) => _IconLibrary(
|
||||
sourceUrl: json['sourceUrl'] as String,
|
||||
name: json['name'] == null ? '' : _stringOrEmpty(json['name']),
|
||||
description: json['description'] == null
|
||||
? ''
|
||||
: _stringOrEmpty(json['description']),
|
||||
icons:
|
||||
(json['icons'] as List<dynamic>?)
|
||||
?.map((e) => IconEntry.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const <IconEntry>[],
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$IconLibraryToJson(_IconLibrary instance) =>
|
||||
<String, dynamic>{
|
||||
'sourceUrl': instance.sourceUrl,
|
||||
'name': instance.name,
|
||||
'description': instance.description,
|
||||
'icons': instance.icons,
|
||||
};
|
||||
@@ -0,0 +1,722 @@
|
||||
|
||||
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'library.freezed.dart';
|
||||
part 'library.g.dart';
|
||||
|
||||
@freezed
|
||||
abstract class EmbyRawUserData with _$EmbyRawUserData {
|
||||
const factory EmbyRawUserData({
|
||||
@JsonKey(fromJson: _intOrNull, includeIfNull: false)
|
||||
int? PlaybackPositionTicks,
|
||||
@JsonKey(fromJson: _intOrNull, includeIfNull: false) int? PlayCount,
|
||||
@JsonKey(fromJson: _intOrNull, includeIfNull: false) int? UnplayedItemCount,
|
||||
@JsonKey(includeIfNull: false) bool? IsFavorite,
|
||||
@JsonKey(includeIfNull: false) bool? Played,
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@Default(<String, dynamic>{})
|
||||
Map<String, dynamic> extra,
|
||||
}) = _EmbyRawUserData;
|
||||
|
||||
factory EmbyRawUserData.fromJson(Map<String, dynamic> json) =>
|
||||
_$EmbyRawUserDataFromJson(json).copyWith(extra: json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class EmbyRawItem with _$EmbyRawItem {
|
||||
const factory EmbyRawItem({
|
||||
required String Id,
|
||||
required String Name,
|
||||
@JsonKey(includeIfNull: false) String? Type,
|
||||
@JsonKey(includeIfNull: false) String? Overview,
|
||||
@JsonKey(fromJson: _intOrNull, includeIfNull: false) int? ProductionYear,
|
||||
@JsonKey(fromJson: _doubleOrNull, includeIfNull: false)
|
||||
double? CommunityRating,
|
||||
@JsonKey(includeIfNull: false) String? PremiereDate,
|
||||
@JsonKey(includeIfNull: false) String? SeriesName,
|
||||
@JsonKey(includeIfNull: false) String? SeriesId,
|
||||
@JsonKey(includeIfNull: false) String? SeasonId,
|
||||
@JsonKey(includeIfNull: false) String? SeasonName,
|
||||
@JsonKey(fromJson: _intOrNull, includeIfNull: false) int? IndexNumber,
|
||||
@JsonKey(includeIfNull: false) String? ParentId,
|
||||
@JsonKey(includeIfNull: false) String? PrimaryImageTag,
|
||||
@JsonKey(fromJson: _stringMapOrNull, includeIfNull: false)
|
||||
Map<String, String>? ImageTags,
|
||||
@JsonKey(fromJson: _stringListOrNull, includeIfNull: false)
|
||||
List<String>? BackdropImageTags,
|
||||
@JsonKey(fromJson: _intOrNull, includeIfNull: false) int? RunTimeTicks,
|
||||
@JsonKey(includeIfNull: false) EmbyRawUserData? UserData,
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@Default(<String, dynamic>{})
|
||||
Map<String, dynamic> extra,
|
||||
}) = _EmbyRawItem;
|
||||
|
||||
factory EmbyRawItem.fromJson(Map<String, dynamic> json) =>
|
||||
_$EmbyRawItemFromJson(json).copyWith(extra: json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class EmbyRawSeason with _$EmbyRawSeason {
|
||||
const factory EmbyRawSeason({
|
||||
required String Id,
|
||||
required String Name,
|
||||
@JsonKey(fromJson: _intOrNull, includeIfNull: false) int? IndexNumber,
|
||||
@JsonKey(fromJson: _intOrNull, includeIfNull: false) int? ProductionYear,
|
||||
@JsonKey(fromJson: _intOrNull, includeIfNull: false) int? ChildCount,
|
||||
@JsonKey(includeIfNull: false) String? PrimaryImageTag,
|
||||
@JsonKey(fromJson: _stringMapOrNull, includeIfNull: false)
|
||||
Map<String, String>? ImageTags,
|
||||
}) = _EmbyRawSeason;
|
||||
|
||||
factory EmbyRawSeason.fromJson(Map<String, dynamic> json) =>
|
||||
_$EmbyRawSeasonFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class EmbyRawLibrary with _$EmbyRawLibrary {
|
||||
const factory EmbyRawLibrary({
|
||||
required String Id,
|
||||
required String Name,
|
||||
@JsonKey(includeIfNull: false) String? CollectionType,
|
||||
@JsonKey(fromJson: _stringMapOrNull, includeIfNull: false)
|
||||
Map<String, String>? ImageTags,
|
||||
}) = _EmbyRawLibrary;
|
||||
|
||||
factory EmbyRawLibrary.fromJson(Map<String, dynamic> json) =>
|
||||
_$EmbyRawLibraryFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class EmbyRawLibraries with _$EmbyRawLibraries {
|
||||
const factory EmbyRawLibraries({required List<EmbyRawLibrary> Items}) =
|
||||
_EmbyRawLibraries;
|
||||
|
||||
factory EmbyRawLibraries.fromJson(Map<String, dynamic> json) =>
|
||||
_$EmbyRawLibrariesFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class EmbyRawMediaStream with _$EmbyRawMediaStream {
|
||||
const factory EmbyRawMediaStream({
|
||||
@JsonKey(fromJson: _intOrNull, includeIfNull: false) int? Index,
|
||||
@JsonKey(includeIfNull: false) String? Type,
|
||||
@JsonKey(includeIfNull: false) String? Language,
|
||||
@JsonKey(includeIfNull: false) String? Codec,
|
||||
@JsonKey(fromJson: _intOrNull, includeIfNull: false) int? Channels,
|
||||
@JsonKey(includeIfNull: false) String? DisplayTitle,
|
||||
@JsonKey(includeIfNull: false) String? Title,
|
||||
@JsonKey(includeIfNull: false) bool? IsDefault,
|
||||
@JsonKey(includeIfNull: false) bool? IsForced,
|
||||
@JsonKey(includeIfNull: false) bool? IsExternal,
|
||||
@JsonKey(includeIfNull: false) String? DeliveryMethod,
|
||||
@JsonKey(includeIfNull: false) String? DeliveryUrl,
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@Default(<String, dynamic>{})
|
||||
Map<String, dynamic> extra,
|
||||
}) = _EmbyRawMediaStream;
|
||||
|
||||
factory EmbyRawMediaStream.fromJson(Map<String, dynamic> json) =>
|
||||
_$EmbyRawMediaStreamFromJson(json).copyWith(extra: json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class EmbyRawMediaSource with _$EmbyRawMediaSource {
|
||||
const factory EmbyRawMediaSource({
|
||||
@JsonKey(includeIfNull: false) String? Id,
|
||||
@JsonKey(includeIfNull: false) String? Name,
|
||||
@JsonKey(includeIfNull: false) String? Container,
|
||||
@JsonKey(fromJson: _intOrNull, includeIfNull: false) int? Bitrate,
|
||||
@JsonKey(fromJson: _intOrNull, includeIfNull: false) int? Size,
|
||||
@JsonKey(fromJson: _intOrNull, includeIfNull: false) int? Width,
|
||||
@JsonKey(fromJson: _intOrNull, includeIfNull: false) int? Height,
|
||||
@JsonKey(fromJson: _intOrNull, includeIfNull: false)
|
||||
int? DefaultSubtitleStreamIndex,
|
||||
@JsonKey(fromJson: _intOrNull, includeIfNull: false)
|
||||
int? DefaultAudioStreamIndex,
|
||||
@JsonKey(includeIfNull: false) List<EmbyRawMediaStream>? MediaStreams,
|
||||
}) = _EmbyRawMediaSource;
|
||||
|
||||
factory EmbyRawMediaSource.fromJson(Map<String, dynamic> json) =>
|
||||
_$EmbyRawMediaSourceFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class LibraryLatestReq with _$LibraryLatestReq {
|
||||
const LibraryLatestReq._();
|
||||
const factory LibraryLatestReq({required String parentId, int? limit}) =
|
||||
_LibraryLatestReq;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'parentId': parentId,
|
||||
if (limit != null) 'limit': limit,
|
||||
};
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class LibraryLatestRes with _$LibraryLatestRes {
|
||||
const factory LibraryLatestRes({
|
||||
required String parentId,
|
||||
required List<EmbyRawItem> items,
|
||||
@Default(0) int totalCount,
|
||||
}) = _LibraryLatestRes;
|
||||
|
||||
factory LibraryLatestRes.fromJson(Map<String, dynamic> json) =>
|
||||
_$LibraryLatestResFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class LibraryResumeRes with _$LibraryResumeRes {
|
||||
const factory LibraryResumeRes({required List<EmbyRawItem> Items}) =
|
||||
_LibraryResumeRes;
|
||||
|
||||
factory LibraryResumeRes.fromJson(Map<String, dynamic> json) =>
|
||||
_$LibraryResumeResFromJson(json);
|
||||
}
|
||||
|
||||
typedef LibraryViewsRes = EmbyRawLibraries;
|
||||
|
||||
@freezed
|
||||
abstract class MediaDetailReq with _$MediaDetailReq {
|
||||
const MediaDetailReq._();
|
||||
const factory MediaDetailReq({required String itemId}) = _MediaDetailReq;
|
||||
|
||||
Map<String, dynamic> toJson() => {'itemId': itemId};
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class EmbyRawMediaDetailSeriesExtra
|
||||
with _$EmbyRawMediaDetailSeriesExtra {
|
||||
const factory EmbyRawMediaDetailSeriesExtra({
|
||||
required List<EmbyRawItem> nextUp,
|
||||
required List<EmbyRawSeason> seasons,
|
||||
}) = _EmbyRawMediaDetailSeriesExtra;
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class EmbyRawMediaDetailSeriesContext
|
||||
with _$EmbyRawMediaDetailSeriesContext {
|
||||
const factory EmbyRawMediaDetailSeriesContext({
|
||||
required EmbyRawItem seriesBase,
|
||||
required EmbyRawMediaDetailSeriesExtra seriesExtra,
|
||||
}) = _EmbyRawMediaDetailSeriesContext;
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class MediaDetailRes with _$MediaDetailRes {
|
||||
const factory MediaDetailRes({
|
||||
required EmbyRawItem base,
|
||||
EmbyRawMediaDetailSeriesExtra? seriesExtra,
|
||||
EmbyRawMediaDetailSeriesContext? seriesContext,
|
||||
List<EmbyRawItem>? similarItems,
|
||||
List<EmbyRawItem>? boxSetItems,
|
||||
}) = _MediaDetailRes;
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class FavoriteSetReq with _$FavoriteSetReq {
|
||||
const FavoriteSetReq._();
|
||||
const factory FavoriteSetReq({
|
||||
required String itemId,
|
||||
required bool isFavorite,
|
||||
}) = _FavoriteSetReq;
|
||||
|
||||
Map<String, dynamic> toJson() => {'itemId': itemId, 'isFavorite': isFavorite};
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class FavoriteSetRes with _$FavoriteSetRes {
|
||||
const factory FavoriteSetRes({
|
||||
required String itemId,
|
||||
required bool isFavorite,
|
||||
}) = _FavoriteSetRes;
|
||||
|
||||
factory FavoriteSetRes.fromJson(Map<String, dynamic> json) =>
|
||||
_$FavoriteSetResFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class PlayedSetReq with _$PlayedSetReq {
|
||||
const PlayedSetReq._();
|
||||
const factory PlayedSetReq({required String itemId, required bool played}) =
|
||||
_PlayedSetReq;
|
||||
|
||||
Map<String, dynamic> toJson() => {'itemId': itemId, 'played': played};
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class PlayedSetRes with _$PlayedSetRes {
|
||||
const factory PlayedSetRes({required String itemId, required bool played}) =
|
||||
_PlayedSetRes;
|
||||
|
||||
factory PlayedSetRes.fromJson(Map<String, dynamic> json) =>
|
||||
_$PlayedSetResFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class HideFromResumeReq with _$HideFromResumeReq {
|
||||
const HideFromResumeReq._();
|
||||
const factory HideFromResumeReq({
|
||||
required String itemId,
|
||||
required bool hide,
|
||||
}) = _HideFromResumeReq;
|
||||
|
||||
Map<String, dynamic> toJson() => {'itemId': itemId, 'hide': hide};
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class HideFromResumeRes with _$HideFromResumeRes {
|
||||
const factory HideFromResumeRes({
|
||||
required String itemId,
|
||||
required bool hide,
|
||||
}) = _HideFromResumeRes;
|
||||
|
||||
factory HideFromResumeRes.fromJson(Map<String, dynamic> json) =>
|
||||
_$HideFromResumeResFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class MediaEpisodesReq with _$MediaEpisodesReq {
|
||||
const MediaEpisodesReq._();
|
||||
const factory MediaEpisodesReq({
|
||||
required String seriesId,
|
||||
required String seasonId,
|
||||
}) = _MediaEpisodesReq;
|
||||
|
||||
Map<String, dynamic> toJson() => {'seriesId': seriesId, 'seasonId': seasonId};
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class MediaEpisodesRes with _$MediaEpisodesRes {
|
||||
const factory MediaEpisodesRes({
|
||||
required String seriesId,
|
||||
required String seasonId,
|
||||
required List<EmbyRawItem> items,
|
||||
}) = _MediaEpisodesRes;
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class SeriesSeasonsReq with _$SeriesSeasonsReq {
|
||||
const SeriesSeasonsReq._();
|
||||
const factory SeriesSeasonsReq({required String seriesId}) =
|
||||
_SeriesSeasonsReq;
|
||||
|
||||
Map<String, dynamic> toJson() => {'seriesId': seriesId};
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class SeriesSeasonsRes with _$SeriesSeasonsRes {
|
||||
const factory SeriesSeasonsRes({
|
||||
required String seriesId,
|
||||
required List<EmbyRawSeason> items,
|
||||
}) = _SeriesSeasonsRes;
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class SimilarItemsReq with _$SimilarItemsReq {
|
||||
const SimilarItemsReq._();
|
||||
const factory SimilarItemsReq({required String itemId, int? limit}) =
|
||||
_SimilarItemsReq;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'itemId': itemId,
|
||||
if (limit != null) 'limit': limit,
|
||||
};
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class SimilarItemsRes with _$SimilarItemsRes {
|
||||
const factory SimilarItemsRes({
|
||||
required String itemId,
|
||||
required List<EmbyRawItem> items,
|
||||
}) = _SimilarItemsRes;
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class AdditionalPartsReq with _$AdditionalPartsReq {
|
||||
const factory AdditionalPartsReq({required String itemId}) =
|
||||
_AdditionalPartsReq;
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class AdditionalPartsRes with _$AdditionalPartsRes {
|
||||
const factory AdditionalPartsRes({
|
||||
required String itemId,
|
||||
required List<EmbyRawItem> items,
|
||||
}) = _AdditionalPartsRes;
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class CrossServerSourceCard with _$CrossServerSourceCard {
|
||||
const factory CrossServerSourceCard({
|
||||
required String serverId,
|
||||
required String serverName,
|
||||
required String serverUrl,
|
||||
required String token,
|
||||
required String userId,
|
||||
required String landingItemId,
|
||||
required String mediaSourceId,
|
||||
required EmbyRawItem item,
|
||||
required EmbyRawMediaSource mediaSource,
|
||||
MediaQualityInfo? quality,
|
||||
}) = _CrossServerSourceCard;
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class MediaQualityInfo with _$MediaQualityInfo {
|
||||
const MediaQualityInfo._();
|
||||
const factory MediaQualityInfo({
|
||||
int? height,
|
||||
int? width,
|
||||
String? videoCodec,
|
||||
String? container,
|
||||
String? videoRange,
|
||||
String? videoRangeType,
|
||||
int? bitDepth,
|
||||
double? fps,
|
||||
}) = _MediaQualityInfo;
|
||||
|
||||
factory MediaQualityInfo.fromMediaSource(EmbyRawMediaSource source) {
|
||||
final streams = source.MediaStreams ?? const [];
|
||||
for (final stream in streams) {
|
||||
if (stream.Type != 'Video') continue;
|
||||
final ex = stream.extra;
|
||||
final realFps = ex['RealFrameRate'] as num?;
|
||||
final avgFps = ex['AverageFrameRate'] as num?;
|
||||
return MediaQualityInfo(
|
||||
height: (ex['Height'] as num?)?.toInt() ?? source.Height,
|
||||
width: (ex['Width'] as num?)?.toInt() ?? source.Width,
|
||||
videoCodec: stream.Codec,
|
||||
container: source.Container,
|
||||
videoRange: ex['VideoRange'] as String?,
|
||||
videoRangeType: ex['VideoRangeType'] as String?,
|
||||
bitDepth: (ex['BitDepth'] as num?)?.toInt(),
|
||||
fps: realFps?.toDouble() ?? avgFps?.toDouble(),
|
||||
);
|
||||
}
|
||||
final nameFallback = fromSourceName(
|
||||
source.Name,
|
||||
fallbackHeight: source.Height,
|
||||
container: source.Container,
|
||||
);
|
||||
if (nameFallback != null) return nameFallback;
|
||||
return MediaQualityInfo(
|
||||
height: source.Height,
|
||||
width: source.Width,
|
||||
container: source.Container,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
static MediaQualityInfo? fromSourceName(
|
||||
String? name, {
|
||||
int? fallbackHeight,
|
||||
String? container,
|
||||
}) {
|
||||
final height = _heightFromName(name) ?? fallbackHeight;
|
||||
final range = _rangeFromName(name);
|
||||
if (height == null && range == null) return null;
|
||||
return MediaQualityInfo(
|
||||
height: height,
|
||||
container: container,
|
||||
videoRange: range,
|
||||
);
|
||||
}
|
||||
|
||||
static int? _heightFromName(String? name) {
|
||||
if (name == null || name.isEmpty) return null;
|
||||
final lower = name.toLowerCase();
|
||||
if (RegExp(r'(?:^|[^0-9])2160(?![0-9])p?|\b4k\b|\buhd\b').hasMatch(lower)) {
|
||||
return 2160;
|
||||
}
|
||||
if (RegExp(r'(?:^|[^0-9])1440(?![0-9])p?|\b2k\b').hasMatch(lower)) {
|
||||
return 1440;
|
||||
}
|
||||
if (RegExp(r'(?:^|[^0-9])1080(?![0-9])p?|\bfhd\b').hasMatch(lower)) {
|
||||
return 1080;
|
||||
}
|
||||
if (RegExp(r'(?:^|[^0-9])720(?![0-9])p?|\bhd\b').hasMatch(lower)) {
|
||||
return 720;
|
||||
}
|
||||
if (RegExp(r'(?:^|[^0-9])480(?![0-9])p?').hasMatch(lower)) return 480;
|
||||
return null;
|
||||
}
|
||||
|
||||
static String? _rangeFromName(String? name) {
|
||||
if (name == null || name.isEmpty) return null;
|
||||
final lower = name.toLowerCase();
|
||||
if (RegExp(r'\bdv\b|dolby[\s._-]*vision|dovi').hasMatch(lower)) {
|
||||
return 'DolbyVision';
|
||||
}
|
||||
if (RegExp(r'hdr10\+|hdr10plus').hasMatch(lower)) return 'HDR10Plus';
|
||||
if (RegExp(r'\bhlg\b').hasMatch(lower)) return 'HLG';
|
||||
if (RegExp(r'hdr10').hasMatch(lower)) return 'HDR10';
|
||||
if (RegExp(r'\bhdr\b').hasMatch(lower)) return 'HDR';
|
||||
return null;
|
||||
}
|
||||
|
||||
String get resolutionLabel {
|
||||
final h = height;
|
||||
if (h == null) return '';
|
||||
if (h >= 2160) return '4K';
|
||||
if (h >= 1440) return '2K';
|
||||
if (h >= 1080) return '1080P';
|
||||
if (h >= 720) return '720P';
|
||||
return '${h}P';
|
||||
}
|
||||
|
||||
String get dynamicRangeLabel {
|
||||
final typeKnown = _canonicalDynamicRange(videoRangeType);
|
||||
if (typeKnown != null) return typeKnown;
|
||||
final rangeKnown = _canonicalDynamicRange(videoRange);
|
||||
if (rangeKnown != null) return rangeKnown;
|
||||
final typeRaw = _rawDynamicRange(videoRangeType);
|
||||
if (typeRaw != null) return typeRaw;
|
||||
final rangeRaw = _rawDynamicRange(videoRange);
|
||||
if (rangeRaw != null) return rangeRaw;
|
||||
return 'SDR';
|
||||
}
|
||||
|
||||
|
||||
static String? _canonicalDynamicRange(String? raw) {
|
||||
if (raw == null) return null;
|
||||
final compact = raw.replaceAll(' ', '');
|
||||
if (compact.isEmpty) return null;
|
||||
final upper = compact.toUpperCase();
|
||||
if (upper.contains('DOVI') || upper.contains('DOLBYVISION')) {
|
||||
return 'Dolby Vision';
|
||||
}
|
||||
if (upper == 'HDR10PLUS' || upper == 'HDR10+') return 'HDR10+';
|
||||
if (upper == 'HDR10') return 'HDR10';
|
||||
if (upper == 'HLG') return 'HLG';
|
||||
if (upper == 'HDR') return 'HDR';
|
||||
if (upper == 'SDR') return 'SDR';
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
static String? _rawDynamicRange(String? raw) {
|
||||
if (raw == null) return null;
|
||||
final trimmed = raw.trim();
|
||||
return trimmed.isEmpty ? null : trimmed;
|
||||
}
|
||||
|
||||
bool get isDolbyVision => dynamicRangeLabel == 'Dolby Vision';
|
||||
bool get isHDR => dynamicRangeLabel != 'SDR';
|
||||
|
||||
String get codecLabel {
|
||||
final c = videoCodec?.toLowerCase();
|
||||
if (c == null || c.isEmpty) return '';
|
||||
if (c == 'hevc' || c == 'h265') return 'H265';
|
||||
if (c == 'h264' || c == 'avc') return 'H264';
|
||||
if (c == 'av1') return 'AV1';
|
||||
if (c == 'vp9') return 'VP9';
|
||||
return c.toUpperCase();
|
||||
}
|
||||
|
||||
String get bitDepthLabel {
|
||||
if (bitDepth == null) return '';
|
||||
return '${bitDepth}bit';
|
||||
}
|
||||
|
||||
String get fpsLabel {
|
||||
if (fps == null) return '';
|
||||
final f = fps!;
|
||||
if ((f - f.roundToDouble()).abs() < 0.1) return '${f.round()}fps';
|
||||
return '${f.toStringAsFixed(1)}fps';
|
||||
}
|
||||
|
||||
String get detailLine {
|
||||
final parts = <String>[
|
||||
codecLabel,
|
||||
bitDepthLabel,
|
||||
fpsLabel,
|
||||
].where((s) => s.isNotEmpty).toList();
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
String get summary {
|
||||
final parts = <String>[];
|
||||
final res = resolutionLabel;
|
||||
if (res.isNotEmpty) parts.add(res);
|
||||
final dr = dynamicRangeLabel;
|
||||
if (dr.isNotEmpty) parts.add(dr);
|
||||
if (codecLabel.isNotEmpty) parts.add(codecLabel);
|
||||
return parts.join(' · ');
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class CrossServerSearchReq with _$CrossServerSearchReq {
|
||||
const factory CrossServerSearchReq({
|
||||
required String anchorType,
|
||||
required String itemName,
|
||||
required Map<String, String> providerIds,
|
||||
Map<String, String>? seriesProviderIds,
|
||||
int? parentIndexNumber,
|
||||
int? indexNumber,
|
||||
|
||||
|
||||
String? excludedServerId,
|
||||
}) = _CrossServerSearchReq;
|
||||
}
|
||||
|
||||
|
||||
@freezed
|
||||
abstract class TmdbAvailabilityReq with _$TmdbAvailabilityReq {
|
||||
const factory TmdbAvailabilityReq({
|
||||
required String tmdbId,
|
||||
required String mediaType,
|
||||
|
||||
|
||||
String? imdbId,
|
||||
}) = _TmdbAvailabilityReq;
|
||||
}
|
||||
|
||||
|
||||
@freezed
|
||||
abstract class TmdbLibraryHit with _$TmdbLibraryHit {
|
||||
const factory TmdbLibraryHit({
|
||||
required String serverId,
|
||||
required String serverName,
|
||||
required String itemId,
|
||||
required String itemType,
|
||||
required String name,
|
||||
int? productionYear,
|
||||
|
||||
|
||||
int? playbackPositionTicks,
|
||||
|
||||
|
||||
int? runTimeTicks,
|
||||
}) = _TmdbLibraryHit;
|
||||
}
|
||||
|
||||
enum SortOrder {
|
||||
ascending('Ascending'),
|
||||
descending('Descending');
|
||||
|
||||
final String value;
|
||||
const SortOrder(this.value);
|
||||
|
||||
static SortOrder? fromString(String? value) {
|
||||
if (value == null) return null;
|
||||
return SortOrder.values.firstWhere(
|
||||
(e) => e.value == value,
|
||||
orElse: () => SortOrder.ascending,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class LibraryItemsReq with _$LibraryItemsReq {
|
||||
const factory LibraryItemsReq({
|
||||
String? parentId,
|
||||
String? includeItemTypes,
|
||||
List<String>? genreIds,
|
||||
String? searchTerm,
|
||||
String? anyProviderIdEquals,
|
||||
String? fields,
|
||||
String? enableImageTypes,
|
||||
bool? groupProgramsBySeries,
|
||||
int? imageTypeLimit,
|
||||
int? startIndex,
|
||||
int? limit,
|
||||
bool? recursive,
|
||||
String? sortBy,
|
||||
SortOrder? sortOrder,
|
||||
}) = _LibraryItemsReq;
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class LibraryItemsRes with _$LibraryItemsRes {
|
||||
const factory LibraryItemsRes({
|
||||
required String parentId,
|
||||
required List<EmbyRawItem> items,
|
||||
}) = _LibraryItemsRes;
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class ItemCountsRes with _$ItemCountsRes {
|
||||
const factory ItemCountsRes({
|
||||
@JsonKey(name: 'MovieCount', fromJson: _intOrZero) required int movieCount,
|
||||
@JsonKey(name: 'EpisodeCount', fromJson: _intOrZero)
|
||||
required int episodeCount,
|
||||
}) = _ItemCountsRes;
|
||||
|
||||
factory ItemCountsRes.fromJson(Map<String, dynamic> json) =>
|
||||
_$ItemCountsResFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class GlobalSearchReq with _$GlobalSearchReq {
|
||||
const factory GlobalSearchReq({
|
||||
required String keyword,
|
||||
String? includeItemTypes,
|
||||
int? limitPerServer,
|
||||
String? serverId,
|
||||
}) = _GlobalSearchReq;
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class GlobalSearchServerResult with _$GlobalSearchServerResult {
|
||||
const factory GlobalSearchServerResult({
|
||||
required String serverId,
|
||||
required String serverName,
|
||||
required String serverUrl,
|
||||
required String token,
|
||||
required String userId,
|
||||
required List<EmbyRawItem> items,
|
||||
}) = _GlobalSearchServerResult;
|
||||
}
|
||||
|
||||
int? _intOrNull(Object? value) => (value as num?)?.toInt();
|
||||
double? _doubleOrNull(Object? value) => (value as num?)?.toDouble();
|
||||
int _intOrZero(Object? value) => (value as num?)?.toInt() ?? 0;
|
||||
|
||||
|
||||
List<EmbyRawMediaSource> mediaSourcesOfItem(EmbyRawItem? item) {
|
||||
final raw = item?.extra['MediaSources'];
|
||||
if (raw is! List) return const [];
|
||||
return raw
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(EmbyRawMediaSource.fromJson)
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
|
||||
Map<String, String> providerIdsOfItem(EmbyRawItem item) {
|
||||
final raw = item.extra['ProviderIds'];
|
||||
if (raw is! Map) return const {};
|
||||
final ids = <String, String>{};
|
||||
for (final entry in raw.entries) {
|
||||
final key = entry.key?.toString() ?? '';
|
||||
final value = entry.value?.toString() ?? '';
|
||||
if (key.isNotEmpty && value.isNotEmpty) ids[key] = value;
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
Map<String, String> normalizeProviderIds(Map<String, String> raw) {
|
||||
if (raw.isEmpty) return const {};
|
||||
final out = <String, String>{};
|
||||
for (final e in raw.entries) {
|
||||
final k = e.key.trim().toLowerCase();
|
||||
final v = e.value.trim();
|
||||
if (k.isNotEmpty && v.isNotEmpty) out[k] = v;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Map<String, String> normalizedProviderIdsOfItem(EmbyRawItem item) =>
|
||||
normalizeProviderIds(providerIdsOfItem(item));
|
||||
|
||||
Map<String, String>? _stringMapOrNull(Object? value) {
|
||||
if (value is! Map) return null;
|
||||
return value.map((k, v) => MapEntry(k as String, v as String));
|
||||
}
|
||||
|
||||
List<String>? _stringListOrNull(Object? value) {
|
||||
if (value is! List) return null;
|
||||
return value.cast<String>();
|
||||
}
|
||||
Generated
+10615
File diff suppressed because it is too large
Load Diff
Generated
+250
@@ -0,0 +1,250 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'library.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_EmbyRawUserData _$EmbyRawUserDataFromJson(Map<String, dynamic> json) =>
|
||||
_EmbyRawUserData(
|
||||
PlaybackPositionTicks: _intOrNull(json['PlaybackPositionTicks']),
|
||||
PlayCount: _intOrNull(json['PlayCount']),
|
||||
UnplayedItemCount: _intOrNull(json['UnplayedItemCount']),
|
||||
IsFavorite: json['IsFavorite'] as bool?,
|
||||
Played: json['Played'] as bool?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$EmbyRawUserDataToJson(_EmbyRawUserData instance) =>
|
||||
<String, dynamic>{
|
||||
'PlaybackPositionTicks': ?instance.PlaybackPositionTicks,
|
||||
'PlayCount': ?instance.PlayCount,
|
||||
'UnplayedItemCount': ?instance.UnplayedItemCount,
|
||||
'IsFavorite': ?instance.IsFavorite,
|
||||
'Played': ?instance.Played,
|
||||
};
|
||||
|
||||
_EmbyRawItem _$EmbyRawItemFromJson(Map<String, dynamic> json) => _EmbyRawItem(
|
||||
Id: json['Id'] as String,
|
||||
Name: json['Name'] as String,
|
||||
Type: json['Type'] as String?,
|
||||
Overview: json['Overview'] as String?,
|
||||
ProductionYear: _intOrNull(json['ProductionYear']),
|
||||
CommunityRating: _doubleOrNull(json['CommunityRating']),
|
||||
PremiereDate: json['PremiereDate'] as String?,
|
||||
SeriesName: json['SeriesName'] as String?,
|
||||
SeriesId: json['SeriesId'] as String?,
|
||||
SeasonId: json['SeasonId'] as String?,
|
||||
SeasonName: json['SeasonName'] as String?,
|
||||
IndexNumber: _intOrNull(json['IndexNumber']),
|
||||
ParentId: json['ParentId'] as String?,
|
||||
PrimaryImageTag: json['PrimaryImageTag'] as String?,
|
||||
ImageTags: _stringMapOrNull(json['ImageTags']),
|
||||
BackdropImageTags: _stringListOrNull(json['BackdropImageTags']),
|
||||
RunTimeTicks: _intOrNull(json['RunTimeTicks']),
|
||||
UserData: json['UserData'] == null
|
||||
? null
|
||||
: EmbyRawUserData.fromJson(json['UserData'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$EmbyRawItemToJson(_EmbyRawItem instance) =>
|
||||
<String, dynamic>{
|
||||
'Id': instance.Id,
|
||||
'Name': instance.Name,
|
||||
'Type': ?instance.Type,
|
||||
'Overview': ?instance.Overview,
|
||||
'ProductionYear': ?instance.ProductionYear,
|
||||
'CommunityRating': ?instance.CommunityRating,
|
||||
'PremiereDate': ?instance.PremiereDate,
|
||||
'SeriesName': ?instance.SeriesName,
|
||||
'SeriesId': ?instance.SeriesId,
|
||||
'SeasonId': ?instance.SeasonId,
|
||||
'SeasonName': ?instance.SeasonName,
|
||||
'IndexNumber': ?instance.IndexNumber,
|
||||
'ParentId': ?instance.ParentId,
|
||||
'PrimaryImageTag': ?instance.PrimaryImageTag,
|
||||
'ImageTags': ?instance.ImageTags,
|
||||
'BackdropImageTags': ?instance.BackdropImageTags,
|
||||
'RunTimeTicks': ?instance.RunTimeTicks,
|
||||
'UserData': ?instance.UserData,
|
||||
};
|
||||
|
||||
_EmbyRawSeason _$EmbyRawSeasonFromJson(Map<String, dynamic> json) =>
|
||||
_EmbyRawSeason(
|
||||
Id: json['Id'] as String,
|
||||
Name: json['Name'] as String,
|
||||
IndexNumber: _intOrNull(json['IndexNumber']),
|
||||
ProductionYear: _intOrNull(json['ProductionYear']),
|
||||
ChildCount: _intOrNull(json['ChildCount']),
|
||||
PrimaryImageTag: json['PrimaryImageTag'] as String?,
|
||||
ImageTags: _stringMapOrNull(json['ImageTags']),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$EmbyRawSeasonToJson(_EmbyRawSeason instance) =>
|
||||
<String, dynamic>{
|
||||
'Id': instance.Id,
|
||||
'Name': instance.Name,
|
||||
'IndexNumber': ?instance.IndexNumber,
|
||||
'ProductionYear': ?instance.ProductionYear,
|
||||
'ChildCount': ?instance.ChildCount,
|
||||
'PrimaryImageTag': ?instance.PrimaryImageTag,
|
||||
'ImageTags': ?instance.ImageTags,
|
||||
};
|
||||
|
||||
_EmbyRawLibrary _$EmbyRawLibraryFromJson(Map<String, dynamic> json) =>
|
||||
_EmbyRawLibrary(
|
||||
Id: json['Id'] as String,
|
||||
Name: json['Name'] as String,
|
||||
CollectionType: json['CollectionType'] as String?,
|
||||
ImageTags: _stringMapOrNull(json['ImageTags']),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$EmbyRawLibraryToJson(_EmbyRawLibrary instance) =>
|
||||
<String, dynamic>{
|
||||
'Id': instance.Id,
|
||||
'Name': instance.Name,
|
||||
'CollectionType': ?instance.CollectionType,
|
||||
'ImageTags': ?instance.ImageTags,
|
||||
};
|
||||
|
||||
_EmbyRawLibraries _$EmbyRawLibrariesFromJson(Map<String, dynamic> json) =>
|
||||
_EmbyRawLibraries(
|
||||
Items: (json['Items'] as List<dynamic>)
|
||||
.map((e) => EmbyRawLibrary.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$EmbyRawLibrariesToJson(_EmbyRawLibraries instance) =>
|
||||
<String, dynamic>{'Items': instance.Items};
|
||||
|
||||
_EmbyRawMediaStream _$EmbyRawMediaStreamFromJson(Map<String, dynamic> json) =>
|
||||
_EmbyRawMediaStream(
|
||||
Index: _intOrNull(json['Index']),
|
||||
Type: json['Type'] as String?,
|
||||
Language: json['Language'] as String?,
|
||||
Codec: json['Codec'] as String?,
|
||||
Channels: _intOrNull(json['Channels']),
|
||||
DisplayTitle: json['DisplayTitle'] as String?,
|
||||
Title: json['Title'] as String?,
|
||||
IsDefault: json['IsDefault'] as bool?,
|
||||
IsForced: json['IsForced'] as bool?,
|
||||
IsExternal: json['IsExternal'] as bool?,
|
||||
DeliveryMethod: json['DeliveryMethod'] as String?,
|
||||
DeliveryUrl: json['DeliveryUrl'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$EmbyRawMediaStreamToJson(_EmbyRawMediaStream instance) =>
|
||||
<String, dynamic>{
|
||||
'Index': ?instance.Index,
|
||||
'Type': ?instance.Type,
|
||||
'Language': ?instance.Language,
|
||||
'Codec': ?instance.Codec,
|
||||
'Channels': ?instance.Channels,
|
||||
'DisplayTitle': ?instance.DisplayTitle,
|
||||
'Title': ?instance.Title,
|
||||
'IsDefault': ?instance.IsDefault,
|
||||
'IsForced': ?instance.IsForced,
|
||||
'IsExternal': ?instance.IsExternal,
|
||||
'DeliveryMethod': ?instance.DeliveryMethod,
|
||||
'DeliveryUrl': ?instance.DeliveryUrl,
|
||||
};
|
||||
|
||||
_EmbyRawMediaSource _$EmbyRawMediaSourceFromJson(Map<String, dynamic> json) =>
|
||||
_EmbyRawMediaSource(
|
||||
Id: json['Id'] as String?,
|
||||
Name: json['Name'] as String?,
|
||||
Container: json['Container'] as String?,
|
||||
Bitrate: _intOrNull(json['Bitrate']),
|
||||
Size: _intOrNull(json['Size']),
|
||||
Width: _intOrNull(json['Width']),
|
||||
Height: _intOrNull(json['Height']),
|
||||
DefaultSubtitleStreamIndex: _intOrNull(
|
||||
json['DefaultSubtitleStreamIndex'],
|
||||
),
|
||||
DefaultAudioStreamIndex: _intOrNull(json['DefaultAudioStreamIndex']),
|
||||
MediaStreams: (json['MediaStreams'] as List<dynamic>?)
|
||||
?.map((e) => EmbyRawMediaStream.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$EmbyRawMediaSourceToJson(_EmbyRawMediaSource instance) =>
|
||||
<String, dynamic>{
|
||||
'Id': ?instance.Id,
|
||||
'Name': ?instance.Name,
|
||||
'Container': ?instance.Container,
|
||||
'Bitrate': ?instance.Bitrate,
|
||||
'Size': ?instance.Size,
|
||||
'Width': ?instance.Width,
|
||||
'Height': ?instance.Height,
|
||||
'DefaultSubtitleStreamIndex': ?instance.DefaultSubtitleStreamIndex,
|
||||
'DefaultAudioStreamIndex': ?instance.DefaultAudioStreamIndex,
|
||||
'MediaStreams': ?instance.MediaStreams,
|
||||
};
|
||||
|
||||
_LibraryLatestRes _$LibraryLatestResFromJson(Map<String, dynamic> json) =>
|
||||
_LibraryLatestRes(
|
||||
parentId: json['parentId'] as String,
|
||||
items: (json['items'] as List<dynamic>)
|
||||
.map((e) => EmbyRawItem.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
totalCount: (json['totalCount'] as num?)?.toInt() ?? 0,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$LibraryLatestResToJson(_LibraryLatestRes instance) =>
|
||||
<String, dynamic>{
|
||||
'parentId': instance.parentId,
|
||||
'items': instance.items,
|
||||
'totalCount': instance.totalCount,
|
||||
};
|
||||
|
||||
_LibraryResumeRes _$LibraryResumeResFromJson(Map<String, dynamic> json) =>
|
||||
_LibraryResumeRes(
|
||||
Items: (json['Items'] as List<dynamic>)
|
||||
.map((e) => EmbyRawItem.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$LibraryResumeResToJson(_LibraryResumeRes instance) =>
|
||||
<String, dynamic>{'Items': instance.Items};
|
||||
|
||||
_FavoriteSetRes _$FavoriteSetResFromJson(Map<String, dynamic> json) =>
|
||||
_FavoriteSetRes(
|
||||
itemId: json['itemId'] as String,
|
||||
isFavorite: json['isFavorite'] as bool,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$FavoriteSetResToJson(_FavoriteSetRes instance) =>
|
||||
<String, dynamic>{
|
||||
'itemId': instance.itemId,
|
||||
'isFavorite': instance.isFavorite,
|
||||
};
|
||||
|
||||
_PlayedSetRes _$PlayedSetResFromJson(Map<String, dynamic> json) =>
|
||||
_PlayedSetRes(
|
||||
itemId: json['itemId'] as String,
|
||||
played: json['played'] as bool,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$PlayedSetResToJson(_PlayedSetRes instance) =>
|
||||
<String, dynamic>{'itemId': instance.itemId, 'played': instance.played};
|
||||
|
||||
_HideFromResumeRes _$HideFromResumeResFromJson(Map<String, dynamic> json) =>
|
||||
_HideFromResumeRes(
|
||||
itemId: json['itemId'] as String,
|
||||
hide: json['hide'] as bool,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$HideFromResumeResToJson(_HideFromResumeRes instance) =>
|
||||
<String, dynamic>{'itemId': instance.itemId, 'hide': instance.hide};
|
||||
|
||||
_ItemCountsRes _$ItemCountsResFromJson(Map<String, dynamic> json) =>
|
||||
_ItemCountsRes(
|
||||
movieCount: _intOrZero(json['MovieCount']),
|
||||
episodeCount: _intOrZero(json['EpisodeCount']),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ItemCountsResToJson(_ItemCountsRes instance) =>
|
||||
<String, dynamic>{
|
||||
'MovieCount': instance.movieCount,
|
||||
'EpisodeCount': instance.episodeCount,
|
||||
};
|
||||
@@ -0,0 +1,181 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import 'library.dart';
|
||||
|
||||
part 'playback.freezed.dart';
|
||||
part 'playback.g.dart';
|
||||
|
||||
@freezed
|
||||
abstract class PlaybackRequestPayload with _$PlaybackRequestPayload {
|
||||
const factory PlaybackRequestPayload({
|
||||
required String itemId,
|
||||
@JsonKey(includeIfNull: false) String? itemType,
|
||||
@JsonKey(includeIfNull: false) String? mediaSourceId,
|
||||
@JsonKey(includeIfNull: false) int? startPositionTicks,
|
||||
@Default(false) bool playFromStart,
|
||||
}) = _PlaybackRequestPayload;
|
||||
|
||||
factory PlaybackRequestPayload.fromJson(Map<String, dynamic> json) =>
|
||||
_$PlaybackRequestPayloadFromJson(json);
|
||||
}
|
||||
|
||||
|
||||
@freezed
|
||||
abstract class EmbySubtitleTrack with _$EmbySubtitleTrack {
|
||||
const factory EmbySubtitleTrack({
|
||||
@JsonKey(fromJson: _intRequired) required int index,
|
||||
@JsonKey(includeIfNull: false) String? title,
|
||||
@JsonKey(includeIfNull: false) String? language,
|
||||
@JsonKey(fromJson: _boolOrFalse) @Default(false) bool isDefault,
|
||||
@JsonKey(fromJson: _boolOrFalse) @Default(false) bool isForced,
|
||||
@JsonKey(fromJson: _boolOrFalse) @Default(false) bool isExternal,
|
||||
@JsonKey(includeIfNull: false) String? deliveryUrl,
|
||||
@JsonKey(includeIfNull: false) String? codec,
|
||||
}) = _EmbySubtitleTrack;
|
||||
|
||||
factory EmbySubtitleTrack.fromJson(Map<String, dynamic> json) =>
|
||||
_$EmbySubtitleTrackFromJson(json);
|
||||
}
|
||||
|
||||
|
||||
@freezed
|
||||
abstract class EmbyAudioTrack with _$EmbyAudioTrack {
|
||||
const factory EmbyAudioTrack({
|
||||
@JsonKey(fromJson: _intRequired) required int index,
|
||||
@JsonKey(includeIfNull: false) String? title,
|
||||
@JsonKey(includeIfNull: false) String? language,
|
||||
@JsonKey(includeIfNull: false) String? codec,
|
||||
@JsonKey(fromJson: _intOrNull, includeIfNull: false) int? channels,
|
||||
@JsonKey(includeIfNull: false) String? displayTitle,
|
||||
@JsonKey(fromJson: _boolOrFalse) @Default(false) bool isDefault,
|
||||
}) = _EmbyAudioTrack;
|
||||
|
||||
factory EmbyAudioTrack.fromJson(Map<String, dynamic> json) =>
|
||||
_$EmbyAudioTrackFromJson(json);
|
||||
}
|
||||
|
||||
int _intRequired(Object? value) => (value as num).toInt();
|
||||
|
||||
int? _intOrNull(Object? value) => (value as num?)?.toInt();
|
||||
|
||||
bool _boolOrFalse(Object? value) => (value as bool?) ?? false;
|
||||
|
||||
|
||||
class PlaybackRequestResult {
|
||||
|
||||
final String streamUrl;
|
||||
|
||||
|
||||
final String? container;
|
||||
|
||||
|
||||
final String? mimeType;
|
||||
|
||||
|
||||
final bool directStream;
|
||||
|
||||
|
||||
final EmbyRawItem item;
|
||||
|
||||
|
||||
final EmbyRawMediaSource? mediaSource;
|
||||
|
||||
|
||||
final double startPositionSeconds;
|
||||
|
||||
|
||||
final double? durationSeconds;
|
||||
|
||||
|
||||
final List<EmbySubtitleTrack> subtitles;
|
||||
|
||||
|
||||
final int? defaultSubtitleIndex;
|
||||
|
||||
|
||||
final List<EmbyAudioTrack> audioTracks;
|
||||
|
||||
|
||||
final int? defaultAudioStreamIndex;
|
||||
|
||||
|
||||
final String? playSessionId;
|
||||
|
||||
|
||||
final String playMethod;
|
||||
|
||||
const PlaybackRequestResult({
|
||||
required this.streamUrl,
|
||||
this.container,
|
||||
this.mimeType,
|
||||
required this.directStream,
|
||||
required this.item,
|
||||
this.mediaSource,
|
||||
this.startPositionSeconds = 0,
|
||||
this.durationSeconds,
|
||||
this.subtitles = const [],
|
||||
this.defaultSubtitleIndex,
|
||||
this.audioTracks = const [],
|
||||
this.defaultAudioStreamIndex,
|
||||
this.playSessionId,
|
||||
this.playMethod = 'DirectStream',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
class PlaybackReportPayload {
|
||||
final String itemId;
|
||||
final String? mediaSourceId;
|
||||
final String? playSessionId;
|
||||
final int positionTicks;
|
||||
final int? runTimeTicks;
|
||||
final String playMethod;
|
||||
final bool canSeek;
|
||||
final bool isPaused;
|
||||
final bool isMuted;
|
||||
final double playbackRate;
|
||||
final int playlistIndex;
|
||||
final int playlistLength;
|
||||
final String repeatMode;
|
||||
final String? eventName;
|
||||
|
||||
const PlaybackReportPayload({
|
||||
required this.itemId,
|
||||
this.mediaSourceId,
|
||||
this.playSessionId,
|
||||
required this.positionTicks,
|
||||
this.runTimeTicks,
|
||||
this.playMethod = 'DirectStream',
|
||||
this.canSeek = true,
|
||||
this.isPaused = false,
|
||||
this.isMuted = false,
|
||||
this.playbackRate = 1,
|
||||
this.playlistIndex = 0,
|
||||
this.playlistLength = 1,
|
||||
this.repeatMode = 'RepeatNone',
|
||||
this.eventName,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson({bool includeNowPlayingQueue = false}) => {
|
||||
'ItemId': itemId,
|
||||
if (mediaSourceId != null && mediaSourceId!.isNotEmpty)
|
||||
'MediaSourceId': mediaSourceId,
|
||||
if (playSessionId != null && playSessionId!.isNotEmpty)
|
||||
'PlaySessionId': playSessionId,
|
||||
'PositionTicks': positionTicks,
|
||||
if (runTimeTicks != null) 'RunTimeTicks': runTimeTicks,
|
||||
'CanSeek': canSeek,
|
||||
'PlayMethod': playMethod,
|
||||
'PlaylistIndex': playlistIndex,
|
||||
'PlaylistLength': playlistLength,
|
||||
'PlaybackRate': playbackRate,
|
||||
'IsMuted': isMuted,
|
||||
'IsPaused': isPaused,
|
||||
'RepeatMode': repeatMode,
|
||||
if (eventName != null && eventName!.isNotEmpty) 'EventName': eventName,
|
||||
if (includeNowPlayingQueue)
|
||||
'NowPlayingQueue': [
|
||||
{'Id': itemId, 'PlaylistItemId': 'playlistItem$playlistIndex'},
|
||||
],
|
||||
};
|
||||
}
|
||||
Generated
+854
@@ -0,0 +1,854 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'playback.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$PlaybackRequestPayload {
|
||||
|
||||
String get itemId;@JsonKey(includeIfNull: false) String? get itemType;@JsonKey(includeIfNull: false) String? get mediaSourceId;@JsonKey(includeIfNull: false) int? get startPositionTicks; bool get playFromStart;
|
||||
/// Create a copy of PlaybackRequestPayload
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$PlaybackRequestPayloadCopyWith<PlaybackRequestPayload> get copyWith => _$PlaybackRequestPayloadCopyWithImpl<PlaybackRequestPayload>(this as PlaybackRequestPayload, _$identity);
|
||||
|
||||
/// Serializes this PlaybackRequestPayload to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is PlaybackRequestPayload&&(identical(other.itemId, itemId) || other.itemId == itemId)&&(identical(other.itemType, itemType) || other.itemType == itemType)&&(identical(other.mediaSourceId, mediaSourceId) || other.mediaSourceId == mediaSourceId)&&(identical(other.startPositionTicks, startPositionTicks) || other.startPositionTicks == startPositionTicks)&&(identical(other.playFromStart, playFromStart) || other.playFromStart == playFromStart));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,itemId,itemType,mediaSourceId,startPositionTicks,playFromStart);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PlaybackRequestPayload(itemId: $itemId, itemType: $itemType, mediaSourceId: $mediaSourceId, startPositionTicks: $startPositionTicks, playFromStart: $playFromStart)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $PlaybackRequestPayloadCopyWith<$Res> {
|
||||
factory $PlaybackRequestPayloadCopyWith(PlaybackRequestPayload value, $Res Function(PlaybackRequestPayload) _then) = _$PlaybackRequestPayloadCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String itemId,@JsonKey(includeIfNull: false) String? itemType,@JsonKey(includeIfNull: false) String? mediaSourceId,@JsonKey(includeIfNull: false) int? startPositionTicks, bool playFromStart
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$PlaybackRequestPayloadCopyWithImpl<$Res>
|
||||
implements $PlaybackRequestPayloadCopyWith<$Res> {
|
||||
_$PlaybackRequestPayloadCopyWithImpl(this._self, this._then);
|
||||
|
||||
final PlaybackRequestPayload _self;
|
||||
final $Res Function(PlaybackRequestPayload) _then;
|
||||
|
||||
/// Create a copy of PlaybackRequestPayload
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? itemId = null,Object? itemType = freezed,Object? mediaSourceId = freezed,Object? startPositionTicks = freezed,Object? playFromStart = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
itemId: null == itemId ? _self.itemId : itemId // ignore: cast_nullable_to_non_nullable
|
||||
as String,itemType: freezed == itemType ? _self.itemType : itemType // ignore: cast_nullable_to_non_nullable
|
||||
as String?,mediaSourceId: freezed == mediaSourceId ? _self.mediaSourceId : mediaSourceId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,startPositionTicks: freezed == startPositionTicks ? _self.startPositionTicks : startPositionTicks // ignore: cast_nullable_to_non_nullable
|
||||
as int?,playFromStart: null == playFromStart ? _self.playFromStart : playFromStart // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [PlaybackRequestPayload].
|
||||
extension PlaybackRequestPayloadPatterns on PlaybackRequestPayload {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _PlaybackRequestPayload value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _PlaybackRequestPayload() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _PlaybackRequestPayload value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _PlaybackRequestPayload():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _PlaybackRequestPayload value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _PlaybackRequestPayload() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String itemId, @JsonKey(includeIfNull: false) String? itemType, @JsonKey(includeIfNull: false) String? mediaSourceId, @JsonKey(includeIfNull: false) int? startPositionTicks, bool playFromStart)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _PlaybackRequestPayload() when $default != null:
|
||||
return $default(_that.itemId,_that.itemType,_that.mediaSourceId,_that.startPositionTicks,_that.playFromStart);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String itemId, @JsonKey(includeIfNull: false) String? itemType, @JsonKey(includeIfNull: false) String? mediaSourceId, @JsonKey(includeIfNull: false) int? startPositionTicks, bool playFromStart) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _PlaybackRequestPayload():
|
||||
return $default(_that.itemId,_that.itemType,_that.mediaSourceId,_that.startPositionTicks,_that.playFromStart);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String itemId, @JsonKey(includeIfNull: false) String? itemType, @JsonKey(includeIfNull: false) String? mediaSourceId, @JsonKey(includeIfNull: false) int? startPositionTicks, bool playFromStart)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _PlaybackRequestPayload() when $default != null:
|
||||
return $default(_that.itemId,_that.itemType,_that.mediaSourceId,_that.startPositionTicks,_that.playFromStart);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _PlaybackRequestPayload implements PlaybackRequestPayload {
|
||||
const _PlaybackRequestPayload({required this.itemId, @JsonKey(includeIfNull: false) this.itemType, @JsonKey(includeIfNull: false) this.mediaSourceId, @JsonKey(includeIfNull: false) this.startPositionTicks, this.playFromStart = false});
|
||||
factory _PlaybackRequestPayload.fromJson(Map<String, dynamic> json) => _$PlaybackRequestPayloadFromJson(json);
|
||||
|
||||
@override final String itemId;
|
||||
@override@JsonKey(includeIfNull: false) final String? itemType;
|
||||
@override@JsonKey(includeIfNull: false) final String? mediaSourceId;
|
||||
@override@JsonKey(includeIfNull: false) final int? startPositionTicks;
|
||||
@override@JsonKey() final bool playFromStart;
|
||||
|
||||
/// Create a copy of PlaybackRequestPayload
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$PlaybackRequestPayloadCopyWith<_PlaybackRequestPayload> get copyWith => __$PlaybackRequestPayloadCopyWithImpl<_PlaybackRequestPayload>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$PlaybackRequestPayloadToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _PlaybackRequestPayload&&(identical(other.itemId, itemId) || other.itemId == itemId)&&(identical(other.itemType, itemType) || other.itemType == itemType)&&(identical(other.mediaSourceId, mediaSourceId) || other.mediaSourceId == mediaSourceId)&&(identical(other.startPositionTicks, startPositionTicks) || other.startPositionTicks == startPositionTicks)&&(identical(other.playFromStart, playFromStart) || other.playFromStart == playFromStart));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,itemId,itemType,mediaSourceId,startPositionTicks,playFromStart);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PlaybackRequestPayload(itemId: $itemId, itemType: $itemType, mediaSourceId: $mediaSourceId, startPositionTicks: $startPositionTicks, playFromStart: $playFromStart)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$PlaybackRequestPayloadCopyWith<$Res> implements $PlaybackRequestPayloadCopyWith<$Res> {
|
||||
factory _$PlaybackRequestPayloadCopyWith(_PlaybackRequestPayload value, $Res Function(_PlaybackRequestPayload) _then) = __$PlaybackRequestPayloadCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String itemId,@JsonKey(includeIfNull: false) String? itemType,@JsonKey(includeIfNull: false) String? mediaSourceId,@JsonKey(includeIfNull: false) int? startPositionTicks, bool playFromStart
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$PlaybackRequestPayloadCopyWithImpl<$Res>
|
||||
implements _$PlaybackRequestPayloadCopyWith<$Res> {
|
||||
__$PlaybackRequestPayloadCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _PlaybackRequestPayload _self;
|
||||
final $Res Function(_PlaybackRequestPayload) _then;
|
||||
|
||||
/// Create a copy of PlaybackRequestPayload
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? itemId = null,Object? itemType = freezed,Object? mediaSourceId = freezed,Object? startPositionTicks = freezed,Object? playFromStart = null,}) {
|
||||
return _then(_PlaybackRequestPayload(
|
||||
itemId: null == itemId ? _self.itemId : itemId // ignore: cast_nullable_to_non_nullable
|
||||
as String,itemType: freezed == itemType ? _self.itemType : itemType // ignore: cast_nullable_to_non_nullable
|
||||
as String?,mediaSourceId: freezed == mediaSourceId ? _self.mediaSourceId : mediaSourceId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,startPositionTicks: freezed == startPositionTicks ? _self.startPositionTicks : startPositionTicks // ignore: cast_nullable_to_non_nullable
|
||||
as int?,playFromStart: null == playFromStart ? _self.playFromStart : playFromStart // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// @nodoc
|
||||
mixin _$EmbySubtitleTrack {
|
||||
|
||||
@JsonKey(fromJson: _intRequired) int get index;@JsonKey(includeIfNull: false) String? get title;@JsonKey(includeIfNull: false) String? get language;@JsonKey(fromJson: _boolOrFalse) bool get isDefault;@JsonKey(fromJson: _boolOrFalse) bool get isForced;@JsonKey(fromJson: _boolOrFalse) bool get isExternal;@JsonKey(includeIfNull: false) String? get deliveryUrl;@JsonKey(includeIfNull: false) String? get codec;
|
||||
/// Create a copy of EmbySubtitleTrack
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$EmbySubtitleTrackCopyWith<EmbySubtitleTrack> get copyWith => _$EmbySubtitleTrackCopyWithImpl<EmbySubtitleTrack>(this as EmbySubtitleTrack, _$identity);
|
||||
|
||||
/// Serializes this EmbySubtitleTrack to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is EmbySubtitleTrack&&(identical(other.index, index) || other.index == index)&&(identical(other.title, title) || other.title == title)&&(identical(other.language, language) || other.language == language)&&(identical(other.isDefault, isDefault) || other.isDefault == isDefault)&&(identical(other.isForced, isForced) || other.isForced == isForced)&&(identical(other.isExternal, isExternal) || other.isExternal == isExternal)&&(identical(other.deliveryUrl, deliveryUrl) || other.deliveryUrl == deliveryUrl)&&(identical(other.codec, codec) || other.codec == codec));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,index,title,language,isDefault,isForced,isExternal,deliveryUrl,codec);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'EmbySubtitleTrack(index: $index, title: $title, language: $language, isDefault: $isDefault, isForced: $isForced, isExternal: $isExternal, deliveryUrl: $deliveryUrl, codec: $codec)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $EmbySubtitleTrackCopyWith<$Res> {
|
||||
factory $EmbySubtitleTrackCopyWith(EmbySubtitleTrack value, $Res Function(EmbySubtitleTrack) _then) = _$EmbySubtitleTrackCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
@JsonKey(fromJson: _intRequired) int index,@JsonKey(includeIfNull: false) String? title,@JsonKey(includeIfNull: false) String? language,@JsonKey(fromJson: _boolOrFalse) bool isDefault,@JsonKey(fromJson: _boolOrFalse) bool isForced,@JsonKey(fromJson: _boolOrFalse) bool isExternal,@JsonKey(includeIfNull: false) String? deliveryUrl,@JsonKey(includeIfNull: false) String? codec
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$EmbySubtitleTrackCopyWithImpl<$Res>
|
||||
implements $EmbySubtitleTrackCopyWith<$Res> {
|
||||
_$EmbySubtitleTrackCopyWithImpl(this._self, this._then);
|
||||
|
||||
final EmbySubtitleTrack _self;
|
||||
final $Res Function(EmbySubtitleTrack) _then;
|
||||
|
||||
/// Create a copy of EmbySubtitleTrack
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? index = null,Object? title = freezed,Object? language = freezed,Object? isDefault = null,Object? isForced = null,Object? isExternal = null,Object? deliveryUrl = freezed,Object? codec = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
index: null == index ? _self.index : index // ignore: cast_nullable_to_non_nullable
|
||||
as int,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
|
||||
as String?,language: freezed == language ? _self.language : language // ignore: cast_nullable_to_non_nullable
|
||||
as String?,isDefault: null == isDefault ? _self.isDefault : isDefault // ignore: cast_nullable_to_non_nullable
|
||||
as bool,isForced: null == isForced ? _self.isForced : isForced // ignore: cast_nullable_to_non_nullable
|
||||
as bool,isExternal: null == isExternal ? _self.isExternal : isExternal // ignore: cast_nullable_to_non_nullable
|
||||
as bool,deliveryUrl: freezed == deliveryUrl ? _self.deliveryUrl : deliveryUrl // ignore: cast_nullable_to_non_nullable
|
||||
as String?,codec: freezed == codec ? _self.codec : codec // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [EmbySubtitleTrack].
|
||||
extension EmbySubtitleTrackPatterns on EmbySubtitleTrack {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _EmbySubtitleTrack value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _EmbySubtitleTrack() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _EmbySubtitleTrack value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _EmbySubtitleTrack():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _EmbySubtitleTrack value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _EmbySubtitleTrack() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function(@JsonKey(fromJson: _intRequired) int index, @JsonKey(includeIfNull: false) String? title, @JsonKey(includeIfNull: false) String? language, @JsonKey(fromJson: _boolOrFalse) bool isDefault, @JsonKey(fromJson: _boolOrFalse) bool isForced, @JsonKey(fromJson: _boolOrFalse) bool isExternal, @JsonKey(includeIfNull: false) String? deliveryUrl, @JsonKey(includeIfNull: false) String? codec)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _EmbySubtitleTrack() when $default != null:
|
||||
return $default(_that.index,_that.title,_that.language,_that.isDefault,_that.isForced,_that.isExternal,_that.deliveryUrl,_that.codec);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function(@JsonKey(fromJson: _intRequired) int index, @JsonKey(includeIfNull: false) String? title, @JsonKey(includeIfNull: false) String? language, @JsonKey(fromJson: _boolOrFalse) bool isDefault, @JsonKey(fromJson: _boolOrFalse) bool isForced, @JsonKey(fromJson: _boolOrFalse) bool isExternal, @JsonKey(includeIfNull: false) String? deliveryUrl, @JsonKey(includeIfNull: false) String? codec) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _EmbySubtitleTrack():
|
||||
return $default(_that.index,_that.title,_that.language,_that.isDefault,_that.isForced,_that.isExternal,_that.deliveryUrl,_that.codec);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function(@JsonKey(fromJson: _intRequired) int index, @JsonKey(includeIfNull: false) String? title, @JsonKey(includeIfNull: false) String? language, @JsonKey(fromJson: _boolOrFalse) bool isDefault, @JsonKey(fromJson: _boolOrFalse) bool isForced, @JsonKey(fromJson: _boolOrFalse) bool isExternal, @JsonKey(includeIfNull: false) String? deliveryUrl, @JsonKey(includeIfNull: false) String? codec)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _EmbySubtitleTrack() when $default != null:
|
||||
return $default(_that.index,_that.title,_that.language,_that.isDefault,_that.isForced,_that.isExternal,_that.deliveryUrl,_that.codec);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _EmbySubtitleTrack implements EmbySubtitleTrack {
|
||||
const _EmbySubtitleTrack({@JsonKey(fromJson: _intRequired) required this.index, @JsonKey(includeIfNull: false) this.title, @JsonKey(includeIfNull: false) this.language, @JsonKey(fromJson: _boolOrFalse) this.isDefault = false, @JsonKey(fromJson: _boolOrFalse) this.isForced = false, @JsonKey(fromJson: _boolOrFalse) this.isExternal = false, @JsonKey(includeIfNull: false) this.deliveryUrl, @JsonKey(includeIfNull: false) this.codec});
|
||||
factory _EmbySubtitleTrack.fromJson(Map<String, dynamic> json) => _$EmbySubtitleTrackFromJson(json);
|
||||
|
||||
@override@JsonKey(fromJson: _intRequired) final int index;
|
||||
@override@JsonKey(includeIfNull: false) final String? title;
|
||||
@override@JsonKey(includeIfNull: false) final String? language;
|
||||
@override@JsonKey(fromJson: _boolOrFalse) final bool isDefault;
|
||||
@override@JsonKey(fromJson: _boolOrFalse) final bool isForced;
|
||||
@override@JsonKey(fromJson: _boolOrFalse) final bool isExternal;
|
||||
@override@JsonKey(includeIfNull: false) final String? deliveryUrl;
|
||||
@override@JsonKey(includeIfNull: false) final String? codec;
|
||||
|
||||
/// Create a copy of EmbySubtitleTrack
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$EmbySubtitleTrackCopyWith<_EmbySubtitleTrack> get copyWith => __$EmbySubtitleTrackCopyWithImpl<_EmbySubtitleTrack>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$EmbySubtitleTrackToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _EmbySubtitleTrack&&(identical(other.index, index) || other.index == index)&&(identical(other.title, title) || other.title == title)&&(identical(other.language, language) || other.language == language)&&(identical(other.isDefault, isDefault) || other.isDefault == isDefault)&&(identical(other.isForced, isForced) || other.isForced == isForced)&&(identical(other.isExternal, isExternal) || other.isExternal == isExternal)&&(identical(other.deliveryUrl, deliveryUrl) || other.deliveryUrl == deliveryUrl)&&(identical(other.codec, codec) || other.codec == codec));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,index,title,language,isDefault,isForced,isExternal,deliveryUrl,codec);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'EmbySubtitleTrack(index: $index, title: $title, language: $language, isDefault: $isDefault, isForced: $isForced, isExternal: $isExternal, deliveryUrl: $deliveryUrl, codec: $codec)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$EmbySubtitleTrackCopyWith<$Res> implements $EmbySubtitleTrackCopyWith<$Res> {
|
||||
factory _$EmbySubtitleTrackCopyWith(_EmbySubtitleTrack value, $Res Function(_EmbySubtitleTrack) _then) = __$EmbySubtitleTrackCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
@JsonKey(fromJson: _intRequired) int index,@JsonKey(includeIfNull: false) String? title,@JsonKey(includeIfNull: false) String? language,@JsonKey(fromJson: _boolOrFalse) bool isDefault,@JsonKey(fromJson: _boolOrFalse) bool isForced,@JsonKey(fromJson: _boolOrFalse) bool isExternal,@JsonKey(includeIfNull: false) String? deliveryUrl,@JsonKey(includeIfNull: false) String? codec
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$EmbySubtitleTrackCopyWithImpl<$Res>
|
||||
implements _$EmbySubtitleTrackCopyWith<$Res> {
|
||||
__$EmbySubtitleTrackCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _EmbySubtitleTrack _self;
|
||||
final $Res Function(_EmbySubtitleTrack) _then;
|
||||
|
||||
/// Create a copy of EmbySubtitleTrack
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? index = null,Object? title = freezed,Object? language = freezed,Object? isDefault = null,Object? isForced = null,Object? isExternal = null,Object? deliveryUrl = freezed,Object? codec = freezed,}) {
|
||||
return _then(_EmbySubtitleTrack(
|
||||
index: null == index ? _self.index : index // ignore: cast_nullable_to_non_nullable
|
||||
as int,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
|
||||
as String?,language: freezed == language ? _self.language : language // ignore: cast_nullable_to_non_nullable
|
||||
as String?,isDefault: null == isDefault ? _self.isDefault : isDefault // ignore: cast_nullable_to_non_nullable
|
||||
as bool,isForced: null == isForced ? _self.isForced : isForced // ignore: cast_nullable_to_non_nullable
|
||||
as bool,isExternal: null == isExternal ? _self.isExternal : isExternal // ignore: cast_nullable_to_non_nullable
|
||||
as bool,deliveryUrl: freezed == deliveryUrl ? _self.deliveryUrl : deliveryUrl // ignore: cast_nullable_to_non_nullable
|
||||
as String?,codec: freezed == codec ? _self.codec : codec // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// @nodoc
|
||||
mixin _$EmbyAudioTrack {
|
||||
|
||||
@JsonKey(fromJson: _intRequired) int get index;@JsonKey(includeIfNull: false) String? get title;@JsonKey(includeIfNull: false) String? get language;@JsonKey(includeIfNull: false) String? get codec;@JsonKey(fromJson: _intOrNull, includeIfNull: false) int? get channels;@JsonKey(includeIfNull: false) String? get displayTitle;@JsonKey(fromJson: _boolOrFalse) bool get isDefault;
|
||||
/// Create a copy of EmbyAudioTrack
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$EmbyAudioTrackCopyWith<EmbyAudioTrack> get copyWith => _$EmbyAudioTrackCopyWithImpl<EmbyAudioTrack>(this as EmbyAudioTrack, _$identity);
|
||||
|
||||
/// Serializes this EmbyAudioTrack to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is EmbyAudioTrack&&(identical(other.index, index) || other.index == index)&&(identical(other.title, title) || other.title == title)&&(identical(other.language, language) || other.language == language)&&(identical(other.codec, codec) || other.codec == codec)&&(identical(other.channels, channels) || other.channels == channels)&&(identical(other.displayTitle, displayTitle) || other.displayTitle == displayTitle)&&(identical(other.isDefault, isDefault) || other.isDefault == isDefault));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,index,title,language,codec,channels,displayTitle,isDefault);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'EmbyAudioTrack(index: $index, title: $title, language: $language, codec: $codec, channels: $channels, displayTitle: $displayTitle, isDefault: $isDefault)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $EmbyAudioTrackCopyWith<$Res> {
|
||||
factory $EmbyAudioTrackCopyWith(EmbyAudioTrack value, $Res Function(EmbyAudioTrack) _then) = _$EmbyAudioTrackCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
@JsonKey(fromJson: _intRequired) int index,@JsonKey(includeIfNull: false) String? title,@JsonKey(includeIfNull: false) String? language,@JsonKey(includeIfNull: false) String? codec,@JsonKey(fromJson: _intOrNull, includeIfNull: false) int? channels,@JsonKey(includeIfNull: false) String? displayTitle,@JsonKey(fromJson: _boolOrFalse) bool isDefault
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$EmbyAudioTrackCopyWithImpl<$Res>
|
||||
implements $EmbyAudioTrackCopyWith<$Res> {
|
||||
_$EmbyAudioTrackCopyWithImpl(this._self, this._then);
|
||||
|
||||
final EmbyAudioTrack _self;
|
||||
final $Res Function(EmbyAudioTrack) _then;
|
||||
|
||||
/// Create a copy of EmbyAudioTrack
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? index = null,Object? title = freezed,Object? language = freezed,Object? codec = freezed,Object? channels = freezed,Object? displayTitle = freezed,Object? isDefault = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
index: null == index ? _self.index : index // ignore: cast_nullable_to_non_nullable
|
||||
as int,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
|
||||
as String?,language: freezed == language ? _self.language : language // ignore: cast_nullable_to_non_nullable
|
||||
as String?,codec: freezed == codec ? _self.codec : codec // ignore: cast_nullable_to_non_nullable
|
||||
as String?,channels: freezed == channels ? _self.channels : channels // ignore: cast_nullable_to_non_nullable
|
||||
as int?,displayTitle: freezed == displayTitle ? _self.displayTitle : displayTitle // ignore: cast_nullable_to_non_nullable
|
||||
as String?,isDefault: null == isDefault ? _self.isDefault : isDefault // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [EmbyAudioTrack].
|
||||
extension EmbyAudioTrackPatterns on EmbyAudioTrack {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _EmbyAudioTrack value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _EmbyAudioTrack() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _EmbyAudioTrack value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _EmbyAudioTrack():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _EmbyAudioTrack value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _EmbyAudioTrack() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function(@JsonKey(fromJson: _intRequired) int index, @JsonKey(includeIfNull: false) String? title, @JsonKey(includeIfNull: false) String? language, @JsonKey(includeIfNull: false) String? codec, @JsonKey(fromJson: _intOrNull, includeIfNull: false) int? channels, @JsonKey(includeIfNull: false) String? displayTitle, @JsonKey(fromJson: _boolOrFalse) bool isDefault)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _EmbyAudioTrack() when $default != null:
|
||||
return $default(_that.index,_that.title,_that.language,_that.codec,_that.channels,_that.displayTitle,_that.isDefault);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function(@JsonKey(fromJson: _intRequired) int index, @JsonKey(includeIfNull: false) String? title, @JsonKey(includeIfNull: false) String? language, @JsonKey(includeIfNull: false) String? codec, @JsonKey(fromJson: _intOrNull, includeIfNull: false) int? channels, @JsonKey(includeIfNull: false) String? displayTitle, @JsonKey(fromJson: _boolOrFalse) bool isDefault) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _EmbyAudioTrack():
|
||||
return $default(_that.index,_that.title,_that.language,_that.codec,_that.channels,_that.displayTitle,_that.isDefault);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function(@JsonKey(fromJson: _intRequired) int index, @JsonKey(includeIfNull: false) String? title, @JsonKey(includeIfNull: false) String? language, @JsonKey(includeIfNull: false) String? codec, @JsonKey(fromJson: _intOrNull, includeIfNull: false) int? channels, @JsonKey(includeIfNull: false) String? displayTitle, @JsonKey(fromJson: _boolOrFalse) bool isDefault)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _EmbyAudioTrack() when $default != null:
|
||||
return $default(_that.index,_that.title,_that.language,_that.codec,_that.channels,_that.displayTitle,_that.isDefault);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _EmbyAudioTrack implements EmbyAudioTrack {
|
||||
const _EmbyAudioTrack({@JsonKey(fromJson: _intRequired) required this.index, @JsonKey(includeIfNull: false) this.title, @JsonKey(includeIfNull: false) this.language, @JsonKey(includeIfNull: false) this.codec, @JsonKey(fromJson: _intOrNull, includeIfNull: false) this.channels, @JsonKey(includeIfNull: false) this.displayTitle, @JsonKey(fromJson: _boolOrFalse) this.isDefault = false});
|
||||
factory _EmbyAudioTrack.fromJson(Map<String, dynamic> json) => _$EmbyAudioTrackFromJson(json);
|
||||
|
||||
@override@JsonKey(fromJson: _intRequired) final int index;
|
||||
@override@JsonKey(includeIfNull: false) final String? title;
|
||||
@override@JsonKey(includeIfNull: false) final String? language;
|
||||
@override@JsonKey(includeIfNull: false) final String? codec;
|
||||
@override@JsonKey(fromJson: _intOrNull, includeIfNull: false) final int? channels;
|
||||
@override@JsonKey(includeIfNull: false) final String? displayTitle;
|
||||
@override@JsonKey(fromJson: _boolOrFalse) final bool isDefault;
|
||||
|
||||
/// Create a copy of EmbyAudioTrack
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$EmbyAudioTrackCopyWith<_EmbyAudioTrack> get copyWith => __$EmbyAudioTrackCopyWithImpl<_EmbyAudioTrack>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$EmbyAudioTrackToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _EmbyAudioTrack&&(identical(other.index, index) || other.index == index)&&(identical(other.title, title) || other.title == title)&&(identical(other.language, language) || other.language == language)&&(identical(other.codec, codec) || other.codec == codec)&&(identical(other.channels, channels) || other.channels == channels)&&(identical(other.displayTitle, displayTitle) || other.displayTitle == displayTitle)&&(identical(other.isDefault, isDefault) || other.isDefault == isDefault));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,index,title,language,codec,channels,displayTitle,isDefault);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'EmbyAudioTrack(index: $index, title: $title, language: $language, codec: $codec, channels: $channels, displayTitle: $displayTitle, isDefault: $isDefault)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$EmbyAudioTrackCopyWith<$Res> implements $EmbyAudioTrackCopyWith<$Res> {
|
||||
factory _$EmbyAudioTrackCopyWith(_EmbyAudioTrack value, $Res Function(_EmbyAudioTrack) _then) = __$EmbyAudioTrackCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
@JsonKey(fromJson: _intRequired) int index,@JsonKey(includeIfNull: false) String? title,@JsonKey(includeIfNull: false) String? language,@JsonKey(includeIfNull: false) String? codec,@JsonKey(fromJson: _intOrNull, includeIfNull: false) int? channels,@JsonKey(includeIfNull: false) String? displayTitle,@JsonKey(fromJson: _boolOrFalse) bool isDefault
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$EmbyAudioTrackCopyWithImpl<$Res>
|
||||
implements _$EmbyAudioTrackCopyWith<$Res> {
|
||||
__$EmbyAudioTrackCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _EmbyAudioTrack _self;
|
||||
final $Res Function(_EmbyAudioTrack) _then;
|
||||
|
||||
/// Create a copy of EmbyAudioTrack
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? index = null,Object? title = freezed,Object? language = freezed,Object? codec = freezed,Object? channels = freezed,Object? displayTitle = freezed,Object? isDefault = null,}) {
|
||||
return _then(_EmbyAudioTrack(
|
||||
index: null == index ? _self.index : index // ignore: cast_nullable_to_non_nullable
|
||||
as int,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
|
||||
as String?,language: freezed == language ? _self.language : language // ignore: cast_nullable_to_non_nullable
|
||||
as String?,codec: freezed == codec ? _self.codec : codec // ignore: cast_nullable_to_non_nullable
|
||||
as String?,channels: freezed == channels ? _self.channels : channels // ignore: cast_nullable_to_non_nullable
|
||||
as int?,displayTitle: freezed == displayTitle ? _self.displayTitle : displayTitle // ignore: cast_nullable_to_non_nullable
|
||||
as String?,isDefault: null == isDefault ? _self.isDefault : isDefault // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
Generated
+81
@@ -0,0 +1,81 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'playback.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_PlaybackRequestPayload _$PlaybackRequestPayloadFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => _PlaybackRequestPayload(
|
||||
itemId: json['itemId'] as String,
|
||||
itemType: json['itemType'] as String?,
|
||||
mediaSourceId: json['mediaSourceId'] as String?,
|
||||
startPositionTicks: (json['startPositionTicks'] as num?)?.toInt(),
|
||||
playFromStart: json['playFromStart'] as bool? ?? false,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$PlaybackRequestPayloadToJson(
|
||||
_PlaybackRequestPayload instance,
|
||||
) => <String, dynamic>{
|
||||
'itemId': instance.itemId,
|
||||
'itemType': ?instance.itemType,
|
||||
'mediaSourceId': ?instance.mediaSourceId,
|
||||
'startPositionTicks': ?instance.startPositionTicks,
|
||||
'playFromStart': instance.playFromStart,
|
||||
};
|
||||
|
||||
_EmbySubtitleTrack _$EmbySubtitleTrackFromJson(Map<String, dynamic> json) =>
|
||||
_EmbySubtitleTrack(
|
||||
index: _intRequired(json['index']),
|
||||
title: json['title'] as String?,
|
||||
language: json['language'] as String?,
|
||||
isDefault: json['isDefault'] == null
|
||||
? false
|
||||
: _boolOrFalse(json['isDefault']),
|
||||
isForced: json['isForced'] == null
|
||||
? false
|
||||
: _boolOrFalse(json['isForced']),
|
||||
isExternal: json['isExternal'] == null
|
||||
? false
|
||||
: _boolOrFalse(json['isExternal']),
|
||||
deliveryUrl: json['deliveryUrl'] as String?,
|
||||
codec: json['codec'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$EmbySubtitleTrackToJson(_EmbySubtitleTrack instance) =>
|
||||
<String, dynamic>{
|
||||
'index': instance.index,
|
||||
'title': ?instance.title,
|
||||
'language': ?instance.language,
|
||||
'isDefault': instance.isDefault,
|
||||
'isForced': instance.isForced,
|
||||
'isExternal': instance.isExternal,
|
||||
'deliveryUrl': ?instance.deliveryUrl,
|
||||
'codec': ?instance.codec,
|
||||
};
|
||||
|
||||
_EmbyAudioTrack _$EmbyAudioTrackFromJson(Map<String, dynamic> json) =>
|
||||
_EmbyAudioTrack(
|
||||
index: _intRequired(json['index']),
|
||||
title: json['title'] as String?,
|
||||
language: json['language'] as String?,
|
||||
codec: json['codec'] as String?,
|
||||
channels: _intOrNull(json['channels']),
|
||||
displayTitle: json['displayTitle'] as String?,
|
||||
isDefault: json['isDefault'] == null
|
||||
? false
|
||||
: _boolOrFalse(json['isDefault']),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$EmbyAudioTrackToJson(_EmbyAudioTrack instance) =>
|
||||
<String, dynamic>{
|
||||
'index': instance.index,
|
||||
'title': ?instance.title,
|
||||
'language': ?instance.language,
|
||||
'codec': ?instance.codec,
|
||||
'channels': ?instance.channels,
|
||||
'displayTitle': ?instance.displayTitle,
|
||||
'isDefault': instance.isDefault,
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
enum GestureAction {
|
||||
none,
|
||||
playPause,
|
||||
fullscreen,
|
||||
toggleControls,
|
||||
seekBackward,
|
||||
seekForward,
|
||||
previousItem,
|
||||
nextItem,
|
||||
exit,
|
||||
}
|
||||
|
||||
GestureAction gestureActionFromName(String? name, GestureAction fallback) {
|
||||
if (name == null) return fallback;
|
||||
for (final v in GestureAction.values) {
|
||||
if (v.name == name) return v;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'script_widget.freezed.dart';
|
||||
part 'script_widget.g.dart';
|
||||
|
||||
@freezed
|
||||
abstract class ScriptWidgetOption with _$ScriptWidgetOption {
|
||||
const factory ScriptWidgetOption({
|
||||
@Default('') String title,
|
||||
@JsonKey(fromJson: _stringFromAny) @Default('') String value,
|
||||
}) = _ScriptWidgetOption;
|
||||
|
||||
factory ScriptWidgetOption.fromJson(Map<String, dynamic> json) =>
|
||||
_$ScriptWidgetOptionFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class ScriptWidgetBelongTo with _$ScriptWidgetBelongTo {
|
||||
const factory ScriptWidgetBelongTo({
|
||||
@Default('') String paramName,
|
||||
@JsonKey(fromJson: _stringListFromAny)
|
||||
@Default(<String>[])
|
||||
List<String> value,
|
||||
}) = _ScriptWidgetBelongTo;
|
||||
|
||||
factory ScriptWidgetBelongTo.fromJson(Map<String, dynamic> json) =>
|
||||
_$ScriptWidgetBelongToFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class ScriptWidgetParam with _$ScriptWidgetParam {
|
||||
const factory ScriptWidgetParam({
|
||||
required String name,
|
||||
@Default('') String title,
|
||||
@Default('input') String type,
|
||||
@Default('') String description,
|
||||
Object? value,
|
||||
ScriptWidgetBelongTo? belongTo,
|
||||
@Default(<ScriptWidgetOption>[]) List<ScriptWidgetOption> placeholders,
|
||||
@Default(<ScriptWidgetOption>[]) List<ScriptWidgetOption> enumOptions,
|
||||
}) = _ScriptWidgetParam;
|
||||
|
||||
factory ScriptWidgetParam.fromJson(Map<String, dynamic> json) =>
|
||||
_$ScriptWidgetParamFromJson(json);
|
||||
}
|
||||
|
||||
Map<String, Object?> buildScriptWidgetParameterDefaults(
|
||||
List<ScriptWidgetParam> parameters,
|
||||
) {
|
||||
return <String, Object?>{
|
||||
for (final parameter in parameters)
|
||||
parameter.name: _resolveScriptWidgetParameterDefault(parameter),
|
||||
};
|
||||
}
|
||||
|
||||
Object? _resolveScriptWidgetParameterDefault(ScriptWidgetParam parameter) {
|
||||
if (parameter.value != null) return parameter.value;
|
||||
if (parameter.enumOptions.isNotEmpty) {
|
||||
return parameter.enumOptions.first.value;
|
||||
}
|
||||
if (parameter.placeholders.isNotEmpty) {
|
||||
return parameter.placeholders.first.value;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class ScriptWidgetModule with _$ScriptWidgetModule {
|
||||
const factory ScriptWidgetModule({
|
||||
@Default('') String id,
|
||||
required String title,
|
||||
@Default('') String description,
|
||||
@Default(false) bool requiresWebView,
|
||||
required String functionName,
|
||||
@Default(false) bool sectionMode,
|
||||
@Default(3600) int cacheDuration,
|
||||
@Default('list') String type,
|
||||
@Default(<ScriptWidgetParam>[]) List<ScriptWidgetParam> params,
|
||||
}) = _ScriptWidgetModule;
|
||||
|
||||
factory ScriptWidgetModule.fromJson(Map<String, dynamic> json) =>
|
||||
_$ScriptWidgetModuleFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class ScriptWidgetManifest with _$ScriptWidgetManifest {
|
||||
const factory ScriptWidgetManifest({
|
||||
required String id,
|
||||
required String title,
|
||||
@Default('') String description,
|
||||
@Default('') String author,
|
||||
@Default('') String site,
|
||||
@Default('0.0.0') String version,
|
||||
@Default('0.0.1') String requiredVersion,
|
||||
@Default(60) int detailCacheDuration,
|
||||
@Default(<ScriptWidgetParam>[]) List<ScriptWidgetParam> globalParams,
|
||||
@Default(<ScriptWidgetModule>[]) List<ScriptWidgetModule> modules,
|
||||
ScriptWidgetModule? search,
|
||||
}) = _ScriptWidgetManifest;
|
||||
|
||||
factory ScriptWidgetManifest.fromJson(Map<String, dynamic> json) =>
|
||||
_$ScriptWidgetManifestFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class ScriptVideoPerson with _$ScriptVideoPerson {
|
||||
const factory ScriptVideoPerson({
|
||||
@JsonKey(fromJson: _stringFromAny) @Default('') String id,
|
||||
@Default('') String title,
|
||||
@Default('') String avatar,
|
||||
@Default('') String role,
|
||||
}) = _ScriptVideoPerson;
|
||||
|
||||
factory ScriptVideoPerson.fromJson(Map<String, dynamic> json) =>
|
||||
_$ScriptVideoPersonFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class ScriptVideoGenre with _$ScriptVideoGenre {
|
||||
const factory ScriptVideoGenre({
|
||||
@JsonKey(fromJson: _stringFromAny) @Default('') String id,
|
||||
@Default('') String title,
|
||||
}) = _ScriptVideoGenre;
|
||||
|
||||
factory ScriptVideoGenre.fromJson(Map<String, dynamic> json) =>
|
||||
_$ScriptVideoGenreFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class ScriptVideoTrailer with _$ScriptVideoTrailer {
|
||||
const factory ScriptVideoTrailer({
|
||||
@Default('') String coverUrl,
|
||||
@Default('') String url,
|
||||
}) = _ScriptVideoTrailer;
|
||||
|
||||
factory ScriptVideoTrailer.fromJson(Map<String, dynamic> json) =>
|
||||
_$ScriptVideoTrailerFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class ScriptVideoItem with _$ScriptVideoItem {
|
||||
const factory ScriptVideoItem({
|
||||
@JsonKey(fromJson: _stringFromAny) required String id,
|
||||
@Default('link') String type,
|
||||
@Default('') String title,
|
||||
@Default('') String coverUrl,
|
||||
@JsonKey(readValue: _readPosterPath) @Default('') String posterPath,
|
||||
@Default('') String detailPoster,
|
||||
@Default('') String backdropPath,
|
||||
@Default(<String>[]) List<String> backdropPaths,
|
||||
@Default('') String releaseDate,
|
||||
@Default('') String mediaType,
|
||||
@JsonKey(fromJson: _doubleFromAny) double? rating,
|
||||
@Default('') String genreTitle,
|
||||
@Default(<ScriptVideoGenre>[]) List<ScriptVideoGenre> genreItems,
|
||||
@Default(<ScriptVideoPerson>[]) List<ScriptVideoPerson> peoples,
|
||||
@JsonKey(fromJson: _intFromAny) int? duration,
|
||||
@Default('') String durationText,
|
||||
@Default('') String previewUrl,
|
||||
@Default(<ScriptVideoTrailer>[]) List<ScriptVideoTrailer> trailers,
|
||||
@Default('') String videoUrl,
|
||||
@Default('') String link,
|
||||
@JsonKey(fromJson: _intFromAny) int? episode,
|
||||
@Default('') String description,
|
||||
@Default('app') String playerType,
|
||||
@Default(<ScriptVideoItem>[]) List<ScriptVideoItem> childItems,
|
||||
@Default(<ScriptVideoItem>[]) List<ScriptVideoItem> episodeItems,
|
||||
@Default(<ScriptVideoItem>[]) List<ScriptVideoItem> relatedItems,
|
||||
}) = _ScriptVideoItem;
|
||||
|
||||
factory ScriptVideoItem.fromJson(Map<String, dynamic> json) =>
|
||||
_$ScriptVideoItemFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class ScriptVideoResource with _$ScriptVideoResource {
|
||||
const factory ScriptVideoResource({
|
||||
@Default('') String name,
|
||||
@Default('') String description,
|
||||
required String url,
|
||||
@JsonKey(readValue: _readResourceHeaders)
|
||||
@Default(<String, String>{})
|
||||
Map<String, String> customHeaders,
|
||||
@Default('app') String playerType,
|
||||
}) = _ScriptVideoResource;
|
||||
|
||||
factory ScriptVideoResource.fromJson(Map<String, dynamic> json) =>
|
||||
_$ScriptVideoResourceFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class InstalledScriptWidget with _$InstalledScriptWidget {
|
||||
const factory InstalledScriptWidget({
|
||||
required ScriptWidgetManifest manifest,
|
||||
required String scriptSource,
|
||||
@Default('') String sourceUrl,
|
||||
@Default(true) bool enabled,
|
||||
@Default(<String, Object?>{}) Map<String, Object?> globalParams,
|
||||
required DateTime installedAt,
|
||||
required DateTime updatedAt,
|
||||
}) = _InstalledScriptWidget;
|
||||
|
||||
factory InstalledScriptWidget.fromJson(Map<String, dynamic> json) =>
|
||||
_$InstalledScriptWidgetFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class ScriptWidgetCatalogEntry with _$ScriptWidgetCatalogEntry {
|
||||
const factory ScriptWidgetCatalogEntry({
|
||||
required String id,
|
||||
required String title,
|
||||
@Default('') String description,
|
||||
@Default('') String requiredVersion,
|
||||
@Default('') String version,
|
||||
@Default('') String author,
|
||||
required String url,
|
||||
}) = _ScriptWidgetCatalogEntry;
|
||||
|
||||
factory ScriptWidgetCatalogEntry.fromJson(Map<String, dynamic> json) =>
|
||||
_$ScriptWidgetCatalogEntryFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class ScriptWidgetCatalog with _$ScriptWidgetCatalog {
|
||||
const factory ScriptWidgetCatalog({
|
||||
required String title,
|
||||
@Default('') String description,
|
||||
@Default('') String icon,
|
||||
@Default(<ScriptWidgetCatalogEntry>[])
|
||||
List<ScriptWidgetCatalogEntry> widgets,
|
||||
}) = _ScriptWidgetCatalog;
|
||||
|
||||
factory ScriptWidgetCatalog.fromJson(Map<String, dynamic> json) =>
|
||||
_$ScriptWidgetCatalogFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class ScriptWidgetSubscription with _$ScriptWidgetSubscription {
|
||||
const factory ScriptWidgetSubscription({
|
||||
required String url,
|
||||
required ScriptWidgetCatalog catalog,
|
||||
required DateTime refreshedAt,
|
||||
}) = _ScriptWidgetSubscription;
|
||||
|
||||
factory ScriptWidgetSubscription.fromJson(Map<String, dynamic> json) =>
|
||||
_$ScriptWidgetSubscriptionFromJson(json);
|
||||
}
|
||||
|
||||
String _stringFromAny(Object? value) => value?.toString() ?? '';
|
||||
|
||||
int? _intFromAny(Object? value) {
|
||||
if (value is num) return value.toInt();
|
||||
return int.tryParse(value?.toString() ?? '');
|
||||
}
|
||||
|
||||
double? _doubleFromAny(Object? value) {
|
||||
if (value is num) return value.toDouble();
|
||||
return double.tryParse(value?.toString() ?? '');
|
||||
}
|
||||
|
||||
List<String> _stringListFromAny(Object? value) {
|
||||
if (value is List) {
|
||||
return value.map((item) => item.toString()).toList(growable: false);
|
||||
}
|
||||
if (value == null) return const <String>[];
|
||||
return <String>[value.toString()];
|
||||
}
|
||||
|
||||
Object? _readPosterPath(Map<dynamic, dynamic> json, String key) {
|
||||
return json[key] ?? json['posterUrl'] ?? json['poster_url'];
|
||||
}
|
||||
|
||||
Object? _readResourceHeaders(Map<dynamic, dynamic> json, String key) {
|
||||
final value = json[key] ?? json['headers'];
|
||||
if (value is! Map) return const <String, String>{};
|
||||
return <String, String>{
|
||||
for (final entry in value.entries)
|
||||
entry.key.toString(): entry.value.toString(),
|
||||
};
|
||||
}
|
||||
+4108
File diff suppressed because it is too large
Load Diff
Generated
+381
@@ -0,0 +1,381 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'script_widget.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_ScriptWidgetOption _$ScriptWidgetOptionFromJson(Map<String, dynamic> json) =>
|
||||
_ScriptWidgetOption(
|
||||
title: json['title'] as String? ?? '',
|
||||
value: json['value'] == null ? '' : _stringFromAny(json['value']),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ScriptWidgetOptionToJson(_ScriptWidgetOption instance) =>
|
||||
<String, dynamic>{'title': instance.title, 'value': instance.value};
|
||||
|
||||
_ScriptWidgetBelongTo _$ScriptWidgetBelongToFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => _ScriptWidgetBelongTo(
|
||||
paramName: json['paramName'] as String? ?? '',
|
||||
value: json['value'] == null
|
||||
? const <String>[]
|
||||
: _stringListFromAny(json['value']),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ScriptWidgetBelongToToJson(
|
||||
_ScriptWidgetBelongTo instance,
|
||||
) => <String, dynamic>{
|
||||
'paramName': instance.paramName,
|
||||
'value': instance.value,
|
||||
};
|
||||
|
||||
_ScriptWidgetParam _$ScriptWidgetParamFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => _ScriptWidgetParam(
|
||||
name: json['name'] as String,
|
||||
title: json['title'] as String? ?? '',
|
||||
type: json['type'] as String? ?? 'input',
|
||||
description: json['description'] as String? ?? '',
|
||||
value: json['value'],
|
||||
belongTo: json['belongTo'] == null
|
||||
? null
|
||||
: ScriptWidgetBelongTo.fromJson(json['belongTo'] as Map<String, dynamic>),
|
||||
placeholders:
|
||||
(json['placeholders'] as List<dynamic>?)
|
||||
?.map((e) => ScriptWidgetOption.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const <ScriptWidgetOption>[],
|
||||
enumOptions:
|
||||
(json['enumOptions'] as List<dynamic>?)
|
||||
?.map((e) => ScriptWidgetOption.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const <ScriptWidgetOption>[],
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ScriptWidgetParamToJson(_ScriptWidgetParam instance) =>
|
||||
<String, dynamic>{
|
||||
'name': instance.name,
|
||||
'title': instance.title,
|
||||
'type': instance.type,
|
||||
'description': instance.description,
|
||||
'value': instance.value,
|
||||
'belongTo': instance.belongTo,
|
||||
'placeholders': instance.placeholders,
|
||||
'enumOptions': instance.enumOptions,
|
||||
};
|
||||
|
||||
_ScriptWidgetModule _$ScriptWidgetModuleFromJson(Map<String, dynamic> json) =>
|
||||
_ScriptWidgetModule(
|
||||
id: json['id'] as String? ?? '',
|
||||
title: json['title'] as String,
|
||||
description: json['description'] as String? ?? '',
|
||||
requiresWebView: json['requiresWebView'] as bool? ?? false,
|
||||
functionName: json['functionName'] as String,
|
||||
sectionMode: json['sectionMode'] as bool? ?? false,
|
||||
cacheDuration: (json['cacheDuration'] as num?)?.toInt() ?? 3600,
|
||||
type: json['type'] as String? ?? 'list',
|
||||
params:
|
||||
(json['params'] as List<dynamic>?)
|
||||
?.map(
|
||||
(e) => ScriptWidgetParam.fromJson(e as Map<String, dynamic>),
|
||||
)
|
||||
.toList() ??
|
||||
const <ScriptWidgetParam>[],
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ScriptWidgetModuleToJson(_ScriptWidgetModule instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'title': instance.title,
|
||||
'description': instance.description,
|
||||
'requiresWebView': instance.requiresWebView,
|
||||
'functionName': instance.functionName,
|
||||
'sectionMode': instance.sectionMode,
|
||||
'cacheDuration': instance.cacheDuration,
|
||||
'type': instance.type,
|
||||
'params': instance.params,
|
||||
};
|
||||
|
||||
_ScriptWidgetManifest _$ScriptWidgetManifestFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => _ScriptWidgetManifest(
|
||||
id: json['id'] as String,
|
||||
title: json['title'] as String,
|
||||
description: json['description'] as String? ?? '',
|
||||
author: json['author'] as String? ?? '',
|
||||
site: json['site'] as String? ?? '',
|
||||
version: json['version'] as String? ?? '0.0.0',
|
||||
requiredVersion: json['requiredVersion'] as String? ?? '0.0.1',
|
||||
detailCacheDuration: (json['detailCacheDuration'] as num?)?.toInt() ?? 60,
|
||||
globalParams:
|
||||
(json['globalParams'] as List<dynamic>?)
|
||||
?.map((e) => ScriptWidgetParam.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const <ScriptWidgetParam>[],
|
||||
modules:
|
||||
(json['modules'] as List<dynamic>?)
|
||||
?.map((e) => ScriptWidgetModule.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const <ScriptWidgetModule>[],
|
||||
search: json['search'] == null
|
||||
? null
|
||||
: ScriptWidgetModule.fromJson(json['search'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ScriptWidgetManifestToJson(
|
||||
_ScriptWidgetManifest instance,
|
||||
) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'title': instance.title,
|
||||
'description': instance.description,
|
||||
'author': instance.author,
|
||||
'site': instance.site,
|
||||
'version': instance.version,
|
||||
'requiredVersion': instance.requiredVersion,
|
||||
'detailCacheDuration': instance.detailCacheDuration,
|
||||
'globalParams': instance.globalParams,
|
||||
'modules': instance.modules,
|
||||
'search': instance.search,
|
||||
};
|
||||
|
||||
_ScriptVideoPerson _$ScriptVideoPersonFromJson(Map<String, dynamic> json) =>
|
||||
_ScriptVideoPerson(
|
||||
id: json['id'] == null ? '' : _stringFromAny(json['id']),
|
||||
title: json['title'] as String? ?? '',
|
||||
avatar: json['avatar'] as String? ?? '',
|
||||
role: json['role'] as String? ?? '',
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ScriptVideoPersonToJson(_ScriptVideoPerson instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'title': instance.title,
|
||||
'avatar': instance.avatar,
|
||||
'role': instance.role,
|
||||
};
|
||||
|
||||
_ScriptVideoGenre _$ScriptVideoGenreFromJson(Map<String, dynamic> json) =>
|
||||
_ScriptVideoGenre(
|
||||
id: json['id'] == null ? '' : _stringFromAny(json['id']),
|
||||
title: json['title'] as String? ?? '',
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ScriptVideoGenreToJson(_ScriptVideoGenre instance) =>
|
||||
<String, dynamic>{'id': instance.id, 'title': instance.title};
|
||||
|
||||
_ScriptVideoTrailer _$ScriptVideoTrailerFromJson(Map<String, dynamic> json) =>
|
||||
_ScriptVideoTrailer(
|
||||
coverUrl: json['coverUrl'] as String? ?? '',
|
||||
url: json['url'] as String? ?? '',
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ScriptVideoTrailerToJson(_ScriptVideoTrailer instance) =>
|
||||
<String, dynamic>{'coverUrl': instance.coverUrl, 'url': instance.url};
|
||||
|
||||
_ScriptVideoItem _$ScriptVideoItemFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => _ScriptVideoItem(
|
||||
id: _stringFromAny(json['id']),
|
||||
type: json['type'] as String? ?? 'link',
|
||||
title: json['title'] as String? ?? '',
|
||||
coverUrl: json['coverUrl'] as String? ?? '',
|
||||
posterPath: _readPosterPath(json, 'posterPath') as String? ?? '',
|
||||
detailPoster: json['detailPoster'] as String? ?? '',
|
||||
backdropPath: json['backdropPath'] as String? ?? '',
|
||||
backdropPaths:
|
||||
(json['backdropPaths'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList() ??
|
||||
const <String>[],
|
||||
releaseDate: json['releaseDate'] as String? ?? '',
|
||||
mediaType: json['mediaType'] as String? ?? '',
|
||||
rating: _doubleFromAny(json['rating']),
|
||||
genreTitle: json['genreTitle'] as String? ?? '',
|
||||
genreItems:
|
||||
(json['genreItems'] as List<dynamic>?)
|
||||
?.map((e) => ScriptVideoGenre.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const <ScriptVideoGenre>[],
|
||||
peoples:
|
||||
(json['peoples'] as List<dynamic>?)
|
||||
?.map((e) => ScriptVideoPerson.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const <ScriptVideoPerson>[],
|
||||
duration: _intFromAny(json['duration']),
|
||||
durationText: json['durationText'] as String? ?? '',
|
||||
previewUrl: json['previewUrl'] as String? ?? '',
|
||||
trailers:
|
||||
(json['trailers'] as List<dynamic>?)
|
||||
?.map((e) => ScriptVideoTrailer.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const <ScriptVideoTrailer>[],
|
||||
videoUrl: json['videoUrl'] as String? ?? '',
|
||||
link: json['link'] as String? ?? '',
|
||||
episode: _intFromAny(json['episode']),
|
||||
description: json['description'] as String? ?? '',
|
||||
playerType: json['playerType'] as String? ?? 'app',
|
||||
childItems:
|
||||
(json['childItems'] as List<dynamic>?)
|
||||
?.map((e) => ScriptVideoItem.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const <ScriptVideoItem>[],
|
||||
episodeItems:
|
||||
(json['episodeItems'] as List<dynamic>?)
|
||||
?.map((e) => ScriptVideoItem.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const <ScriptVideoItem>[],
|
||||
relatedItems:
|
||||
(json['relatedItems'] as List<dynamic>?)
|
||||
?.map((e) => ScriptVideoItem.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const <ScriptVideoItem>[],
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ScriptVideoItemToJson(_ScriptVideoItem instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'type': instance.type,
|
||||
'title': instance.title,
|
||||
'coverUrl': instance.coverUrl,
|
||||
'posterPath': instance.posterPath,
|
||||
'detailPoster': instance.detailPoster,
|
||||
'backdropPath': instance.backdropPath,
|
||||
'backdropPaths': instance.backdropPaths,
|
||||
'releaseDate': instance.releaseDate,
|
||||
'mediaType': instance.mediaType,
|
||||
'rating': instance.rating,
|
||||
'genreTitle': instance.genreTitle,
|
||||
'genreItems': instance.genreItems,
|
||||
'peoples': instance.peoples,
|
||||
'duration': instance.duration,
|
||||
'durationText': instance.durationText,
|
||||
'previewUrl': instance.previewUrl,
|
||||
'trailers': instance.trailers,
|
||||
'videoUrl': instance.videoUrl,
|
||||
'link': instance.link,
|
||||
'episode': instance.episode,
|
||||
'description': instance.description,
|
||||
'playerType': instance.playerType,
|
||||
'childItems': instance.childItems,
|
||||
'episodeItems': instance.episodeItems,
|
||||
'relatedItems': instance.relatedItems,
|
||||
};
|
||||
|
||||
_ScriptVideoResource _$ScriptVideoResourceFromJson(Map<String, dynamic> json) =>
|
||||
_ScriptVideoResource(
|
||||
name: json['name'] as String? ?? '',
|
||||
description: json['description'] as String? ?? '',
|
||||
url: json['url'] as String,
|
||||
customHeaders:
|
||||
(_readResourceHeaders(json, 'customHeaders') as Map<String, dynamic>?)
|
||||
?.map((k, e) => MapEntry(k, e as String)) ??
|
||||
const <String, String>{},
|
||||
playerType: json['playerType'] as String? ?? 'app',
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ScriptVideoResourceToJson(
|
||||
_ScriptVideoResource instance,
|
||||
) => <String, dynamic>{
|
||||
'name': instance.name,
|
||||
'description': instance.description,
|
||||
'url': instance.url,
|
||||
'customHeaders': instance.customHeaders,
|
||||
'playerType': instance.playerType,
|
||||
};
|
||||
|
||||
_InstalledScriptWidget _$InstalledScriptWidgetFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => _InstalledScriptWidget(
|
||||
manifest: ScriptWidgetManifest.fromJson(
|
||||
json['manifest'] as Map<String, dynamic>,
|
||||
),
|
||||
scriptSource: json['scriptSource'] as String,
|
||||
sourceUrl: json['sourceUrl'] as String? ?? '',
|
||||
enabled: json['enabled'] as bool? ?? true,
|
||||
globalParams:
|
||||
json['globalParams'] as Map<String, dynamic>? ??
|
||||
const <String, Object?>{},
|
||||
installedAt: DateTime.parse(json['installedAt'] as String),
|
||||
updatedAt: DateTime.parse(json['updatedAt'] as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$InstalledScriptWidgetToJson(
|
||||
_InstalledScriptWidget instance,
|
||||
) => <String, dynamic>{
|
||||
'manifest': instance.manifest,
|
||||
'scriptSource': instance.scriptSource,
|
||||
'sourceUrl': instance.sourceUrl,
|
||||
'enabled': instance.enabled,
|
||||
'globalParams': instance.globalParams,
|
||||
'installedAt': instance.installedAt.toIso8601String(),
|
||||
'updatedAt': instance.updatedAt.toIso8601String(),
|
||||
};
|
||||
|
||||
_ScriptWidgetCatalogEntry _$ScriptWidgetCatalogEntryFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => _ScriptWidgetCatalogEntry(
|
||||
id: json['id'] as String,
|
||||
title: json['title'] as String,
|
||||
description: json['description'] as String? ?? '',
|
||||
requiredVersion: json['requiredVersion'] as String? ?? '',
|
||||
version: json['version'] as String? ?? '',
|
||||
author: json['author'] as String? ?? '',
|
||||
url: json['url'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ScriptWidgetCatalogEntryToJson(
|
||||
_ScriptWidgetCatalogEntry instance,
|
||||
) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'title': instance.title,
|
||||
'description': instance.description,
|
||||
'requiredVersion': instance.requiredVersion,
|
||||
'version': instance.version,
|
||||
'author': instance.author,
|
||||
'url': instance.url,
|
||||
};
|
||||
|
||||
_ScriptWidgetCatalog _$ScriptWidgetCatalogFromJson(Map<String, dynamic> json) =>
|
||||
_ScriptWidgetCatalog(
|
||||
title: json['title'] as String,
|
||||
description: json['description'] as String? ?? '',
|
||||
icon: json['icon'] as String? ?? '',
|
||||
widgets:
|
||||
(json['widgets'] as List<dynamic>?)
|
||||
?.map(
|
||||
(e) => ScriptWidgetCatalogEntry.fromJson(
|
||||
e as Map<String, dynamic>,
|
||||
),
|
||||
)
|
||||
.toList() ??
|
||||
const <ScriptWidgetCatalogEntry>[],
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ScriptWidgetCatalogToJson(
|
||||
_ScriptWidgetCatalog instance,
|
||||
) => <String, dynamic>{
|
||||
'title': instance.title,
|
||||
'description': instance.description,
|
||||
'icon': instance.icon,
|
||||
'widgets': instance.widgets,
|
||||
};
|
||||
|
||||
_ScriptWidgetSubscription _$ScriptWidgetSubscriptionFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => _ScriptWidgetSubscription(
|
||||
url: json['url'] as String,
|
||||
catalog: ScriptWidgetCatalog.fromJson(
|
||||
json['catalog'] as Map<String, dynamic>,
|
||||
),
|
||||
refreshedAt: DateTime.parse(json['refreshedAt'] as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ScriptWidgetSubscriptionToJson(
|
||||
_ScriptWidgetSubscription instance,
|
||||
) => <String, dynamic>{
|
||||
'url': instance.url,
|
||||
'catalog': instance.catalog,
|
||||
'refreshedAt': instance.refreshedAt.toIso8601String(),
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'server.freezed.dart';
|
||||
part 'server.g.dart';
|
||||
|
||||
@freezed
|
||||
abstract class EmbyServer with _$EmbyServer {
|
||||
const factory EmbyServer({
|
||||
required String id,
|
||||
required String name,
|
||||
required String baseUrl,
|
||||
required String createdAt,
|
||||
required String updatedAt,
|
||||
@JsonKey(includeIfNull: false) String? lastConnectedAt,
|
||||
|
||||
|
||||
@JsonKey(
|
||||
fromJson: _stringOrEmpty,
|
||||
toJson: _emptyStringToNull,
|
||||
includeIfNull: false,
|
||||
)
|
||||
@Default('')
|
||||
String iconUrl,
|
||||
|
||||
|
||||
@Default(false) bool paused,
|
||||
}) = _EmbyServer;
|
||||
|
||||
factory EmbyServer.fromJson(Map<String, dynamic> json) =>
|
||||
_$EmbyServerFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class EmbyServerSaveReq with _$EmbyServerSaveReq {
|
||||
const factory EmbyServerSaveReq({
|
||||
@JsonKey(includeIfNull: false) String? id,
|
||||
required String name,
|
||||
required String baseUrl,
|
||||
|
||||
|
||||
@JsonKey(
|
||||
fromJson: _stringOrEmpty,
|
||||
toJson: _emptyStringToNull,
|
||||
includeIfNull: false,
|
||||
)
|
||||
@Default('')
|
||||
String iconUrl,
|
||||
}) = _EmbyServerSaveReq;
|
||||
|
||||
factory EmbyServerSaveReq.fromJson(Map<String, dynamic> json) =>
|
||||
_$EmbyServerSaveReqFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class EmbyServerProbeReq with _$EmbyServerProbeReq {
|
||||
const factory EmbyServerProbeReq({required String baseUrl}) =
|
||||
_EmbyServerProbeReq;
|
||||
|
||||
factory EmbyServerProbeReq.fromJson(Map<String, dynamic> json) =>
|
||||
_$EmbyServerProbeReqFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class ProbePublicInfoRes with _$ProbePublicInfoRes {
|
||||
const factory ProbePublicInfoRes({
|
||||
required String serverName,
|
||||
required String version,
|
||||
required String productName,
|
||||
}) = _ProbePublicInfoRes;
|
||||
|
||||
factory ProbePublicInfoRes.fromJson(Map<String, dynamic> json) =>
|
||||
_$ProbePublicInfoResFromJson(json);
|
||||
}
|
||||
|
||||
String _stringOrEmpty(Object? value) => value?.toString() ?? '';
|
||||
|
||||
String? _emptyStringToNull(String value) => value.isEmpty ? null : value;
|
||||
Generated
+1112
File diff suppressed because it is too large
Load Diff
Generated
+66
@@ -0,0 +1,66 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'server.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_EmbyServer _$EmbyServerFromJson(Map<String, dynamic> json) => _EmbyServer(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
baseUrl: json['baseUrl'] as String,
|
||||
createdAt: json['createdAt'] as String,
|
||||
updatedAt: json['updatedAt'] as String,
|
||||
lastConnectedAt: json['lastConnectedAt'] as String?,
|
||||
iconUrl: json['iconUrl'] == null ? '' : _stringOrEmpty(json['iconUrl']),
|
||||
paused: json['paused'] as bool? ?? false,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$EmbyServerToJson(_EmbyServer instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'baseUrl': instance.baseUrl,
|
||||
'createdAt': instance.createdAt,
|
||||
'updatedAt': instance.updatedAt,
|
||||
'lastConnectedAt': ?instance.lastConnectedAt,
|
||||
'iconUrl': ?_emptyStringToNull(instance.iconUrl),
|
||||
'paused': instance.paused,
|
||||
};
|
||||
|
||||
_EmbyServerSaveReq _$EmbyServerSaveReqFromJson(Map<String, dynamic> json) =>
|
||||
_EmbyServerSaveReq(
|
||||
id: json['id'] as String?,
|
||||
name: json['name'] as String,
|
||||
baseUrl: json['baseUrl'] as String,
|
||||
iconUrl: json['iconUrl'] == null ? '' : _stringOrEmpty(json['iconUrl']),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$EmbyServerSaveReqToJson(_EmbyServerSaveReq instance) =>
|
||||
<String, dynamic>{
|
||||
'id': ?instance.id,
|
||||
'name': instance.name,
|
||||
'baseUrl': instance.baseUrl,
|
||||
'iconUrl': ?_emptyStringToNull(instance.iconUrl),
|
||||
};
|
||||
|
||||
_EmbyServerProbeReq _$EmbyServerProbeReqFromJson(Map<String, dynamic> json) =>
|
||||
_EmbyServerProbeReq(baseUrl: json['baseUrl'] as String);
|
||||
|
||||
Map<String, dynamic> _$EmbyServerProbeReqToJson(_EmbyServerProbeReq instance) =>
|
||||
<String, dynamic>{'baseUrl': instance.baseUrl};
|
||||
|
||||
_ProbePublicInfoRes _$ProbePublicInfoResFromJson(Map<String, dynamic> json) =>
|
||||
_ProbePublicInfoRes(
|
||||
serverName: json['serverName'] as String,
|
||||
version: json['version'] as String,
|
||||
productName: json['productName'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ProbePublicInfoResToJson(_ProbePublicInfoRes instance) =>
|
||||
<String, dynamic>{
|
||||
'serverName': instance.serverName,
|
||||
'version': instance.version,
|
||||
'productName': instance.productName,
|
||||
};
|
||||
@@ -0,0 +1,324 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'tmdb.freezed.dart';
|
||||
part 'tmdb.g.dart';
|
||||
|
||||
@freezed
|
||||
abstract class TmdbCastMember with _$TmdbCastMember {
|
||||
const factory TmdbCastMember({
|
||||
@JsonKey(fromJson: _intRequired) required int id,
|
||||
@JsonKey(fromJson: _strOrEmpty) @Default('') String name,
|
||||
String? character,
|
||||
@JsonKey(name: 'profile_path') String? profilePath,
|
||||
@JsonKey(fromJson: _intOrZero) @Default(0) int order,
|
||||
}) = _TmdbCastMember;
|
||||
|
||||
factory TmdbCastMember.fromJson(Map<String, dynamic> json) =>
|
||||
_$TmdbCastMemberFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class TmdbCrewMember with _$TmdbCrewMember {
|
||||
const factory TmdbCrewMember({
|
||||
@JsonKey(fromJson: _intRequired) required int id,
|
||||
@JsonKey(fromJson: _strOrEmpty) @Default('') String name,
|
||||
String? job,
|
||||
String? department,
|
||||
@JsonKey(name: 'profile_path') String? profilePath,
|
||||
}) = _TmdbCrewMember;
|
||||
|
||||
factory TmdbCrewMember.fromJson(Map<String, dynamic> json) =>
|
||||
_$TmdbCrewMemberFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class TmdbCreditsRes with _$TmdbCreditsRes {
|
||||
const factory TmdbCreditsRes({
|
||||
@Default(<TmdbCastMember>[]) List<TmdbCastMember> cast,
|
||||
@Default(<TmdbCrewMember>[]) List<TmdbCrewMember> crew,
|
||||
}) = _TmdbCreditsRes;
|
||||
|
||||
factory TmdbCreditsRes.fromJson(Map<String, dynamic> json) =>
|
||||
_$TmdbCreditsResFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class TmdbRecommendation with _$TmdbRecommendation {
|
||||
const factory TmdbRecommendation({
|
||||
@JsonKey(fromJson: _intRequired) required int id,
|
||||
required String title,
|
||||
@JsonKey(name: 'poster_path') String? posterPath,
|
||||
@JsonKey(name: 'backdrop_path') String? backdropPath,
|
||||
String? overview,
|
||||
@JsonKey(name: 'vote_average', fromJson: _doubleOrNull) double? voteAverage,
|
||||
@JsonKey(name: 'media_type') String? mediaType,
|
||||
}) = _TmdbRecommendation;
|
||||
|
||||
factory TmdbRecommendation.fromJson(Map<String, dynamic> json) =>
|
||||
_$TmdbRecommendationFromJson({
|
||||
...json,
|
||||
'title': json['title'] ?? json['name'] ?? '',
|
||||
});
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class TmdbRecommendationsRes with _$TmdbRecommendationsRes {
|
||||
const factory TmdbRecommendationsRes({
|
||||
@Default(<TmdbRecommendation>[]) List<TmdbRecommendation> results,
|
||||
@JsonKey(name: 'total_results', fromJson: _intOrZero)
|
||||
@Default(0)
|
||||
int totalResults,
|
||||
}) = _TmdbRecommendationsRes;
|
||||
|
||||
factory TmdbRecommendationsRes.fromJson(Map<String, dynamic> json) =>
|
||||
_$TmdbRecommendationsResFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class TmdbEpisode with _$TmdbEpisode {
|
||||
const factory TmdbEpisode({
|
||||
@JsonKey(name: 'episode_number', fromJson: _intRequired)
|
||||
required int episodeNumber,
|
||||
@JsonKey(fromJson: _strOrEmpty) @Default('') String name,
|
||||
String? overview,
|
||||
@JsonKey(name: 'still_path') String? stillPath,
|
||||
@JsonKey(name: 'air_date') String? airDate,
|
||||
@JsonKey(name: 'vote_average', fromJson: _doubleOrNull) double? voteAverage,
|
||||
@JsonKey(fromJson: _intOrNull) int? runtime,
|
||||
}) = _TmdbEpisode;
|
||||
|
||||
factory TmdbEpisode.fromJson(Map<String, dynamic> json) =>
|
||||
_$TmdbEpisodeFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class TmdbSeasonDetail with _$TmdbSeasonDetail {
|
||||
const factory TmdbSeasonDetail({
|
||||
@JsonKey(name: 'season_number', fromJson: _intRequired)
|
||||
required int seasonNumber,
|
||||
@JsonKey(fromJson: _strOrEmpty) @Default('') String name,
|
||||
String? overview,
|
||||
@JsonKey(name: 'poster_path') String? posterPath,
|
||||
@Default(<TmdbEpisode>[]) List<TmdbEpisode> episodes,
|
||||
}) = _TmdbSeasonDetail;
|
||||
|
||||
factory TmdbSeasonDetail.fromJson(Map<String, dynamic> json) =>
|
||||
_$TmdbSeasonDetailFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class TmdbVideo with _$TmdbVideo {
|
||||
const TmdbVideo._();
|
||||
|
||||
const factory TmdbVideo({
|
||||
@JsonKey(fromJson: _strOrEmpty) @Default('') String id,
|
||||
@JsonKey(fromJson: _strOrEmpty) @Default('') String key,
|
||||
@JsonKey(fromJson: _strOrEmpty) @Default('') String name,
|
||||
@JsonKey(fromJson: _strOrEmpty) @Default('') String site,
|
||||
@JsonKey(fromJson: _strOrEmpty) @Default('') String type,
|
||||
@JsonKey(fromJson: _intOrZero) @Default(0) int size,
|
||||
@JsonKey(fromJson: _boolOrFalse) @Default(false) bool official,
|
||||
}) = _TmdbVideo;
|
||||
|
||||
factory TmdbVideo.fromJson(Map<String, dynamic> json) =>
|
||||
_$TmdbVideoFromJson(json);
|
||||
|
||||
String? get youtubeUrl => site == 'YouTube' && key.isNotEmpty
|
||||
? 'https://www.youtube.com/watch?v=$key'
|
||||
: null;
|
||||
|
||||
String? get youtubeThumbnailUrl => site == 'YouTube' && key.isNotEmpty
|
||||
? 'https://img.youtube.com/vi/$key/mqdefault.jpg'
|
||||
: null;
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class TmdbVideosRes with _$TmdbVideosRes {
|
||||
const factory TmdbVideosRes({
|
||||
@Default(<TmdbVideo>[]) List<TmdbVideo> results,
|
||||
}) = _TmdbVideosRes;
|
||||
|
||||
factory TmdbVideosRes.fromJson(Map<String, dynamic> json) =>
|
||||
_$TmdbVideosResFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class TmdbPersonDetail with _$TmdbPersonDetail {
|
||||
const factory TmdbPersonDetail({
|
||||
@JsonKey(fromJson: _intRequired) required int id,
|
||||
@JsonKey(fromJson: _strOrEmpty) @Default('') String name,
|
||||
String? biography,
|
||||
String? birthday,
|
||||
String? deathday,
|
||||
@JsonKey(name: 'place_of_birth') String? placeOfBirth,
|
||||
@JsonKey(name: 'profile_path') String? profilePath,
|
||||
@JsonKey(name: 'known_for_department') String? knownForDepartment,
|
||||
@JsonKey(fromJson: _doubleOrNull) double? popularity,
|
||||
}) = _TmdbPersonDetail;
|
||||
|
||||
factory TmdbPersonDetail.fromJson(Map<String, dynamic> json) =>
|
||||
_$TmdbPersonDetailFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class TmdbPersonCredit with _$TmdbPersonCredit {
|
||||
const factory TmdbPersonCredit({
|
||||
@JsonKey(fromJson: _intRequired) required int id,
|
||||
required String title,
|
||||
String? character,
|
||||
@JsonKey(name: 'poster_path') String? posterPath,
|
||||
@JsonKey(name: 'release_date') String? releaseDate,
|
||||
@JsonKey(name: 'vote_average', fromJson: _doubleOrNull) double? voteAverage,
|
||||
@JsonKey(name: 'media_type') String? mediaType,
|
||||
}) = _TmdbPersonCredit;
|
||||
|
||||
factory TmdbPersonCredit.fromJson(Map<String, dynamic> json) =>
|
||||
_$TmdbPersonCreditFromJson({
|
||||
...json,
|
||||
'title': json['title'] ?? json['name'] ?? '',
|
||||
'release_date': json['release_date'] ?? json['first_air_date'],
|
||||
});
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class TmdbPersonCreditsRes with _$TmdbPersonCreditsRes {
|
||||
const factory TmdbPersonCreditsRes({
|
||||
@Default(<TmdbPersonCredit>[]) List<TmdbPersonCredit> cast,
|
||||
}) = _TmdbPersonCreditsRes;
|
||||
|
||||
factory TmdbPersonCreditsRes.fromJson(Map<String, dynamic> json) =>
|
||||
_$TmdbPersonCreditsResFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class TmdbGenre with _$TmdbGenre {
|
||||
const factory TmdbGenre({
|
||||
@JsonKey(fromJson: _intRequired) required int id,
|
||||
@JsonKey(fromJson: _strOrEmpty) @Default('') String name,
|
||||
}) = _TmdbGenre;
|
||||
|
||||
factory TmdbGenre.fromJson(Map<String, dynamic> json) =>
|
||||
_$TmdbGenreFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class TmdbMediaDetail with _$TmdbMediaDetail {
|
||||
const factory TmdbMediaDetail({
|
||||
@JsonKey(fromJson: _intRequired) required int id,
|
||||
@JsonKey(fromJson: _strOrEmpty) @Default('') String title,
|
||||
@JsonKey(name: 'poster_path') String? posterPath,
|
||||
@JsonKey(name: 'backdrop_path') String? backdropPath,
|
||||
@JsonKey(name: 'release_date') String? releaseDate,
|
||||
@Default(<TmdbGenre>[]) List<TmdbGenre> genres,
|
||||
@JsonKey(fromJson: _intOrNull) int? runtime,
|
||||
String? tagline,
|
||||
String? overview,
|
||||
String? status,
|
||||
String? homepage,
|
||||
@JsonKey(name: 'vote_average', fromJson: _doubleOrNull) double? voteAverage,
|
||||
@JsonKey(name: 'vote_count', fromJson: _intOrNull) int? voteCount,
|
||||
@JsonKey(fromJson: _intOrNull) int? revenue,
|
||||
@JsonKey(fromJson: _intOrNull) int? budget,
|
||||
}) = _TmdbMediaDetail;
|
||||
|
||||
|
||||
factory TmdbMediaDetail.fromJson(Map<String, dynamic> json) =>
|
||||
_$TmdbMediaDetailFromJson({
|
||||
...json,
|
||||
'title': json['title'] ?? json['name'] ?? '',
|
||||
'release_date': json['release_date'] ?? json['first_air_date'],
|
||||
'runtime':
|
||||
json['runtime'] ??
|
||||
(json['episode_run_time'] is List &&
|
||||
(json['episode_run_time'] as List).isNotEmpty
|
||||
? (json['episode_run_time'] as List).first
|
||||
: null),
|
||||
});
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class TmdbImage with _$TmdbImage {
|
||||
const TmdbImage._();
|
||||
|
||||
const factory TmdbImage({
|
||||
@JsonKey(name: 'file_path', fromJson: _strOrEmpty)
|
||||
@Default('')
|
||||
String filePath,
|
||||
@JsonKey(fromJson: _intOrZero) @Default(0) int width,
|
||||
@JsonKey(fromJson: _intOrZero) @Default(0) int height,
|
||||
@JsonKey(name: 'vote_average', fromJson: _doubleOrNull) double? voteAverage,
|
||||
@JsonKey(name: 'iso_639_1') String? iso639,
|
||||
}) = _TmdbImage;
|
||||
|
||||
factory TmdbImage.fromJson(Map<String, dynamic> json) =>
|
||||
_$TmdbImageFromJson(json);
|
||||
|
||||
bool get isLanguageNeutral => iso639 == null;
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class TmdbImageSet with _$TmdbImageSet {
|
||||
const factory TmdbImageSet({
|
||||
@Default(<TmdbImage>[]) List<TmdbImage> backdrops,
|
||||
@Default(<TmdbImage>[]) List<TmdbImage> posters,
|
||||
@Default(<TmdbImage>[]) List<TmdbImage> logos,
|
||||
}) = _TmdbImageSet;
|
||||
|
||||
factory TmdbImageSet.fromJson(Map<String, dynamic> json) =>
|
||||
_$TmdbImageSetFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class TmdbNetwork with _$TmdbNetwork {
|
||||
const factory TmdbNetwork({
|
||||
@JsonKey(fromJson: _intRequired) required int id,
|
||||
@JsonKey(fromJson: _strOrEmpty) @Default('') String name,
|
||||
@JsonKey(name: 'logo_path') String? logoPath,
|
||||
@JsonKey(name: 'origin_country') String? originCountry,
|
||||
}) = _TmdbNetwork;
|
||||
|
||||
factory TmdbNetwork.fromJson(Map<String, dynamic> json) =>
|
||||
_$TmdbNetworkFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class TmdbEnrichedDetail with _$TmdbEnrichedDetail {
|
||||
const factory TmdbEnrichedDetail({
|
||||
required TmdbMediaDetail detail,
|
||||
TmdbVideosRes? videos,
|
||||
TmdbCreditsRes? credits,
|
||||
TmdbRecommendationsRes? recommendations,
|
||||
TmdbImageSet? images,
|
||||
|
||||
|
||||
String? imdbId,
|
||||
@Default(<TmdbNetwork>[]) List<TmdbNetwork> networks,
|
||||
}) = _TmdbEnrichedDetail;
|
||||
|
||||
factory TmdbEnrichedDetail.fromJson(Map<String, dynamic> json) =>
|
||||
_$TmdbEnrichedDetailFromJson({
|
||||
'detail': json,
|
||||
'videos': json['videos'],
|
||||
'credits': json['credits'],
|
||||
'recommendations': json['recommendations'],
|
||||
'images': json['images'],
|
||||
'imdbId': _imdbIdOf(json['external_ids']),
|
||||
'networks': json['networks'],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
String? _imdbIdOf(Object? externalIds) {
|
||||
if (externalIds is! Map) return null;
|
||||
final v = externalIds['imdb_id'];
|
||||
if (v is! String) return null;
|
||||
final trimmed = v.trim();
|
||||
return trimmed.isEmpty ? null : trimmed;
|
||||
}
|
||||
|
||||
int _intRequired(Object? v) => (v as num).toInt();
|
||||
int _intOrZero(Object? v) => (v as num?)?.toInt() ?? 0;
|
||||
int? _intOrNull(Object? v) => (v as num?)?.toInt();
|
||||
double? _doubleOrNull(Object? v) => (v as num?)?.toDouble();
|
||||
bool _boolOrFalse(Object? v) => (v as bool?) ?? false;
|
||||
String _strOrEmpty(Object? v) => v?.toString() ?? '';
|
||||
Generated
+5162
File diff suppressed because it is too large
Load Diff
Generated
+384
@@ -0,0 +1,384 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'tmdb.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_TmdbCastMember _$TmdbCastMemberFromJson(Map<String, dynamic> json) =>
|
||||
_TmdbCastMember(
|
||||
id: _intRequired(json['id']),
|
||||
name: json['name'] == null ? '' : _strOrEmpty(json['name']),
|
||||
character: json['character'] as String?,
|
||||
profilePath: json['profile_path'] as String?,
|
||||
order: json['order'] == null ? 0 : _intOrZero(json['order']),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$TmdbCastMemberToJson(_TmdbCastMember instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'character': instance.character,
|
||||
'profile_path': instance.profilePath,
|
||||
'order': instance.order,
|
||||
};
|
||||
|
||||
_TmdbCrewMember _$TmdbCrewMemberFromJson(Map<String, dynamic> json) =>
|
||||
_TmdbCrewMember(
|
||||
id: _intRequired(json['id']),
|
||||
name: json['name'] == null ? '' : _strOrEmpty(json['name']),
|
||||
job: json['job'] as String?,
|
||||
department: json['department'] as String?,
|
||||
profilePath: json['profile_path'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$TmdbCrewMemberToJson(_TmdbCrewMember instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'job': instance.job,
|
||||
'department': instance.department,
|
||||
'profile_path': instance.profilePath,
|
||||
};
|
||||
|
||||
_TmdbCreditsRes _$TmdbCreditsResFromJson(Map<String, dynamic> json) =>
|
||||
_TmdbCreditsRes(
|
||||
cast:
|
||||
(json['cast'] as List<dynamic>?)
|
||||
?.map((e) => TmdbCastMember.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const <TmdbCastMember>[],
|
||||
crew:
|
||||
(json['crew'] as List<dynamic>?)
|
||||
?.map((e) => TmdbCrewMember.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const <TmdbCrewMember>[],
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$TmdbCreditsResToJson(_TmdbCreditsRes instance) =>
|
||||
<String, dynamic>{'cast': instance.cast, 'crew': instance.crew};
|
||||
|
||||
_TmdbRecommendation _$TmdbRecommendationFromJson(Map<String, dynamic> json) =>
|
||||
_TmdbRecommendation(
|
||||
id: _intRequired(json['id']),
|
||||
title: json['title'] as String,
|
||||
posterPath: json['poster_path'] as String?,
|
||||
backdropPath: json['backdrop_path'] as String?,
|
||||
overview: json['overview'] as String?,
|
||||
voteAverage: _doubleOrNull(json['vote_average']),
|
||||
mediaType: json['media_type'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$TmdbRecommendationToJson(_TmdbRecommendation instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'title': instance.title,
|
||||
'poster_path': instance.posterPath,
|
||||
'backdrop_path': instance.backdropPath,
|
||||
'overview': instance.overview,
|
||||
'vote_average': instance.voteAverage,
|
||||
'media_type': instance.mediaType,
|
||||
};
|
||||
|
||||
_TmdbRecommendationsRes _$TmdbRecommendationsResFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => _TmdbRecommendationsRes(
|
||||
results:
|
||||
(json['results'] as List<dynamic>?)
|
||||
?.map((e) => TmdbRecommendation.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const <TmdbRecommendation>[],
|
||||
totalResults: json['total_results'] == null
|
||||
? 0
|
||||
: _intOrZero(json['total_results']),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$TmdbRecommendationsResToJson(
|
||||
_TmdbRecommendationsRes instance,
|
||||
) => <String, dynamic>{
|
||||
'results': instance.results,
|
||||
'total_results': instance.totalResults,
|
||||
};
|
||||
|
||||
_TmdbEpisode _$TmdbEpisodeFromJson(Map<String, dynamic> json) => _TmdbEpisode(
|
||||
episodeNumber: _intRequired(json['episode_number']),
|
||||
name: json['name'] == null ? '' : _strOrEmpty(json['name']),
|
||||
overview: json['overview'] as String?,
|
||||
stillPath: json['still_path'] as String?,
|
||||
airDate: json['air_date'] as String?,
|
||||
voteAverage: _doubleOrNull(json['vote_average']),
|
||||
runtime: _intOrNull(json['runtime']),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$TmdbEpisodeToJson(_TmdbEpisode instance) =>
|
||||
<String, dynamic>{
|
||||
'episode_number': instance.episodeNumber,
|
||||
'name': instance.name,
|
||||
'overview': instance.overview,
|
||||
'still_path': instance.stillPath,
|
||||
'air_date': instance.airDate,
|
||||
'vote_average': instance.voteAverage,
|
||||
'runtime': instance.runtime,
|
||||
};
|
||||
|
||||
_TmdbSeasonDetail _$TmdbSeasonDetailFromJson(Map<String, dynamic> json) =>
|
||||
_TmdbSeasonDetail(
|
||||
seasonNumber: _intRequired(json['season_number']),
|
||||
name: json['name'] == null ? '' : _strOrEmpty(json['name']),
|
||||
overview: json['overview'] as String?,
|
||||
posterPath: json['poster_path'] as String?,
|
||||
episodes:
|
||||
(json['episodes'] as List<dynamic>?)
|
||||
?.map((e) => TmdbEpisode.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const <TmdbEpisode>[],
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$TmdbSeasonDetailToJson(_TmdbSeasonDetail instance) =>
|
||||
<String, dynamic>{
|
||||
'season_number': instance.seasonNumber,
|
||||
'name': instance.name,
|
||||
'overview': instance.overview,
|
||||
'poster_path': instance.posterPath,
|
||||
'episodes': instance.episodes,
|
||||
};
|
||||
|
||||
_TmdbVideo _$TmdbVideoFromJson(Map<String, dynamic> json) => _TmdbVideo(
|
||||
id: json['id'] == null ? '' : _strOrEmpty(json['id']),
|
||||
key: json['key'] == null ? '' : _strOrEmpty(json['key']),
|
||||
name: json['name'] == null ? '' : _strOrEmpty(json['name']),
|
||||
site: json['site'] == null ? '' : _strOrEmpty(json['site']),
|
||||
type: json['type'] == null ? '' : _strOrEmpty(json['type']),
|
||||
size: json['size'] == null ? 0 : _intOrZero(json['size']),
|
||||
official: json['official'] == null ? false : _boolOrFalse(json['official']),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$TmdbVideoToJson(_TmdbVideo instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'key': instance.key,
|
||||
'name': instance.name,
|
||||
'site': instance.site,
|
||||
'type': instance.type,
|
||||
'size': instance.size,
|
||||
'official': instance.official,
|
||||
};
|
||||
|
||||
_TmdbVideosRes _$TmdbVideosResFromJson(Map<String, dynamic> json) =>
|
||||
_TmdbVideosRes(
|
||||
results:
|
||||
(json['results'] as List<dynamic>?)
|
||||
?.map((e) => TmdbVideo.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const <TmdbVideo>[],
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$TmdbVideosResToJson(_TmdbVideosRes instance) =>
|
||||
<String, dynamic>{'results': instance.results};
|
||||
|
||||
_TmdbPersonDetail _$TmdbPersonDetailFromJson(Map<String, dynamic> json) =>
|
||||
_TmdbPersonDetail(
|
||||
id: _intRequired(json['id']),
|
||||
name: json['name'] == null ? '' : _strOrEmpty(json['name']),
|
||||
biography: json['biography'] as String?,
|
||||
birthday: json['birthday'] as String?,
|
||||
deathday: json['deathday'] as String?,
|
||||
placeOfBirth: json['place_of_birth'] as String?,
|
||||
profilePath: json['profile_path'] as String?,
|
||||
knownForDepartment: json['known_for_department'] as String?,
|
||||
popularity: _doubleOrNull(json['popularity']),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$TmdbPersonDetailToJson(_TmdbPersonDetail instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'biography': instance.biography,
|
||||
'birthday': instance.birthday,
|
||||
'deathday': instance.deathday,
|
||||
'place_of_birth': instance.placeOfBirth,
|
||||
'profile_path': instance.profilePath,
|
||||
'known_for_department': instance.knownForDepartment,
|
||||
'popularity': instance.popularity,
|
||||
};
|
||||
|
||||
_TmdbPersonCredit _$TmdbPersonCreditFromJson(Map<String, dynamic> json) =>
|
||||
_TmdbPersonCredit(
|
||||
id: _intRequired(json['id']),
|
||||
title: json['title'] as String,
|
||||
character: json['character'] as String?,
|
||||
posterPath: json['poster_path'] as String?,
|
||||
releaseDate: json['release_date'] as String?,
|
||||
voteAverage: _doubleOrNull(json['vote_average']),
|
||||
mediaType: json['media_type'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$TmdbPersonCreditToJson(_TmdbPersonCredit instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'title': instance.title,
|
||||
'character': instance.character,
|
||||
'poster_path': instance.posterPath,
|
||||
'release_date': instance.releaseDate,
|
||||
'vote_average': instance.voteAverage,
|
||||
'media_type': instance.mediaType,
|
||||
};
|
||||
|
||||
_TmdbPersonCreditsRes _$TmdbPersonCreditsResFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => _TmdbPersonCreditsRes(
|
||||
cast:
|
||||
(json['cast'] as List<dynamic>?)
|
||||
?.map((e) => TmdbPersonCredit.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const <TmdbPersonCredit>[],
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$TmdbPersonCreditsResToJson(
|
||||
_TmdbPersonCreditsRes instance,
|
||||
) => <String, dynamic>{'cast': instance.cast};
|
||||
|
||||
_TmdbGenre _$TmdbGenreFromJson(Map<String, dynamic> json) => _TmdbGenre(
|
||||
id: _intRequired(json['id']),
|
||||
name: json['name'] == null ? '' : _strOrEmpty(json['name']),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$TmdbGenreToJson(_TmdbGenre instance) =>
|
||||
<String, dynamic>{'id': instance.id, 'name': instance.name};
|
||||
|
||||
_TmdbMediaDetail _$TmdbMediaDetailFromJson(Map<String, dynamic> json) =>
|
||||
_TmdbMediaDetail(
|
||||
id: _intRequired(json['id']),
|
||||
title: json['title'] == null ? '' : _strOrEmpty(json['title']),
|
||||
posterPath: json['poster_path'] as String?,
|
||||
backdropPath: json['backdrop_path'] as String?,
|
||||
releaseDate: json['release_date'] as String?,
|
||||
genres:
|
||||
(json['genres'] as List<dynamic>?)
|
||||
?.map((e) => TmdbGenre.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const <TmdbGenre>[],
|
||||
runtime: _intOrNull(json['runtime']),
|
||||
tagline: json['tagline'] as String?,
|
||||
overview: json['overview'] as String?,
|
||||
status: json['status'] as String?,
|
||||
homepage: json['homepage'] as String?,
|
||||
voteAverage: _doubleOrNull(json['vote_average']),
|
||||
voteCount: _intOrNull(json['vote_count']),
|
||||
revenue: _intOrNull(json['revenue']),
|
||||
budget: _intOrNull(json['budget']),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$TmdbMediaDetailToJson(_TmdbMediaDetail instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'title': instance.title,
|
||||
'poster_path': instance.posterPath,
|
||||
'backdrop_path': instance.backdropPath,
|
||||
'release_date': instance.releaseDate,
|
||||
'genres': instance.genres,
|
||||
'runtime': instance.runtime,
|
||||
'tagline': instance.tagline,
|
||||
'overview': instance.overview,
|
||||
'status': instance.status,
|
||||
'homepage': instance.homepage,
|
||||
'vote_average': instance.voteAverage,
|
||||
'vote_count': instance.voteCount,
|
||||
'revenue': instance.revenue,
|
||||
'budget': instance.budget,
|
||||
};
|
||||
|
||||
_TmdbImage _$TmdbImageFromJson(Map<String, dynamic> json) => _TmdbImage(
|
||||
filePath: json['file_path'] == null ? '' : _strOrEmpty(json['file_path']),
|
||||
width: json['width'] == null ? 0 : _intOrZero(json['width']),
|
||||
height: json['height'] == null ? 0 : _intOrZero(json['height']),
|
||||
voteAverage: _doubleOrNull(json['vote_average']),
|
||||
iso639: json['iso_639_1'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$TmdbImageToJson(_TmdbImage instance) =>
|
||||
<String, dynamic>{
|
||||
'file_path': instance.filePath,
|
||||
'width': instance.width,
|
||||
'height': instance.height,
|
||||
'vote_average': instance.voteAverage,
|
||||
'iso_639_1': instance.iso639,
|
||||
};
|
||||
|
||||
_TmdbImageSet _$TmdbImageSetFromJson(Map<String, dynamic> json) =>
|
||||
_TmdbImageSet(
|
||||
backdrops:
|
||||
(json['backdrops'] as List<dynamic>?)
|
||||
?.map((e) => TmdbImage.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const <TmdbImage>[],
|
||||
posters:
|
||||
(json['posters'] as List<dynamic>?)
|
||||
?.map((e) => TmdbImage.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const <TmdbImage>[],
|
||||
logos:
|
||||
(json['logos'] as List<dynamic>?)
|
||||
?.map((e) => TmdbImage.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const <TmdbImage>[],
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$TmdbImageSetToJson(_TmdbImageSet instance) =>
|
||||
<String, dynamic>{
|
||||
'backdrops': instance.backdrops,
|
||||
'posters': instance.posters,
|
||||
'logos': instance.logos,
|
||||
};
|
||||
|
||||
_TmdbNetwork _$TmdbNetworkFromJson(Map<String, dynamic> json) => _TmdbNetwork(
|
||||
id: _intRequired(json['id']),
|
||||
name: json['name'] == null ? '' : _strOrEmpty(json['name']),
|
||||
logoPath: json['logo_path'] as String?,
|
||||
originCountry: json['origin_country'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$TmdbNetworkToJson(_TmdbNetwork instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'logo_path': instance.logoPath,
|
||||
'origin_country': instance.originCountry,
|
||||
};
|
||||
|
||||
_TmdbEnrichedDetail _$TmdbEnrichedDetailFromJson(Map<String, dynamic> json) =>
|
||||
_TmdbEnrichedDetail(
|
||||
detail: TmdbMediaDetail.fromJson(json['detail'] as Map<String, dynamic>),
|
||||
videos: json['videos'] == null
|
||||
? null
|
||||
: TmdbVideosRes.fromJson(json['videos'] as Map<String, dynamic>),
|
||||
credits: json['credits'] == null
|
||||
? null
|
||||
: TmdbCreditsRes.fromJson(json['credits'] as Map<String, dynamic>),
|
||||
recommendations: json['recommendations'] == null
|
||||
? null
|
||||
: TmdbRecommendationsRes.fromJson(
|
||||
json['recommendations'] as Map<String, dynamic>,
|
||||
),
|
||||
images: json['images'] == null
|
||||
? null
|
||||
: TmdbImageSet.fromJson(json['images'] as Map<String, dynamic>),
|
||||
imdbId: json['imdbId'] as String?,
|
||||
networks:
|
||||
(json['networks'] as List<dynamic>?)
|
||||
?.map((e) => TmdbNetwork.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const <TmdbNetwork>[],
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$TmdbEnrichedDetailToJson(_TmdbEnrichedDetail instance) =>
|
||||
<String, dynamic>{
|
||||
'detail': instance.detail,
|
||||
'videos': instance.videos,
|
||||
'credits': instance.credits,
|
||||
'recommendations': instance.recommendations,
|
||||
'images': instance.images,
|
||||
'imdbId': instance.imdbId,
|
||||
'networks': instance.networks,
|
||||
};
|
||||
@@ -0,0 +1,154 @@
|
||||
import 'trakt_ids.dart';
|
||||
|
||||
|
||||
class TraktCalendarEntry {
|
||||
|
||||
final DateTime firstAired;
|
||||
|
||||
|
||||
final String type;
|
||||
|
||||
final String? showTitle;
|
||||
final int? showYear;
|
||||
final TraktIds? showIds;
|
||||
final int? episodeSeason;
|
||||
final int? episodeNumber;
|
||||
final String? episodeTitle;
|
||||
final TraktIds? episodeIds;
|
||||
|
||||
final String? movieTitle;
|
||||
final int? movieYear;
|
||||
final TraktIds? movieIds;
|
||||
|
||||
|
||||
final String? posterUrl;
|
||||
|
||||
const TraktCalendarEntry({
|
||||
required this.firstAired,
|
||||
required this.type,
|
||||
this.showTitle,
|
||||
this.showYear,
|
||||
this.showIds,
|
||||
this.episodeSeason,
|
||||
this.episodeNumber,
|
||||
this.episodeTitle,
|
||||
this.episodeIds,
|
||||
this.movieTitle,
|
||||
this.movieYear,
|
||||
this.movieIds,
|
||||
this.posterUrl,
|
||||
});
|
||||
|
||||
TraktCalendarEntry withPoster(String? url) => TraktCalendarEntry(
|
||||
firstAired: firstAired,
|
||||
type: type,
|
||||
showTitle: showTitle,
|
||||
showYear: showYear,
|
||||
showIds: showIds,
|
||||
episodeSeason: episodeSeason,
|
||||
episodeNumber: episodeNumber,
|
||||
episodeTitle: episodeTitle,
|
||||
episodeIds: episodeIds,
|
||||
movieTitle: movieTitle,
|
||||
movieYear: movieYear,
|
||||
movieIds: movieIds,
|
||||
posterUrl: url,
|
||||
);
|
||||
|
||||
TraktCalendarEntry withTitle(String title) => TraktCalendarEntry(
|
||||
firstAired: firstAired,
|
||||
type: type,
|
||||
showTitle: type == 'movie' ? showTitle : title,
|
||||
showYear: showYear,
|
||||
showIds: showIds,
|
||||
episodeSeason: episodeSeason,
|
||||
episodeNumber: episodeNumber,
|
||||
episodeTitle: episodeTitle,
|
||||
episodeIds: episodeIds,
|
||||
movieTitle: type == 'movie' ? title : movieTitle,
|
||||
movieYear: movieYear,
|
||||
movieIds: movieIds,
|
||||
posterUrl: posterUrl,
|
||||
);
|
||||
|
||||
|
||||
int? get tmdbId => type == 'movie' ? movieIds?.tmdb : showIds?.tmdb;
|
||||
|
||||
|
||||
String get mediaType => type == 'movie' ? 'movie' : 'tv';
|
||||
|
||||
|
||||
String get displayTitle =>
|
||||
type == 'movie' ? (movieTitle ?? '') : (showTitle ?? '');
|
||||
|
||||
|
||||
String get displaySubtitle {
|
||||
if (type == 'movie') return movieYear?.toString() ?? '';
|
||||
final parts = <String>[];
|
||||
if (episodeSeason != null && episodeNumber != null) {
|
||||
parts.add(
|
||||
'S${episodeSeason.toString().padLeft(2, '0')}'
|
||||
'E${episodeNumber.toString().padLeft(2, '0')}',
|
||||
);
|
||||
}
|
||||
if (episodeTitle != null && episodeTitle!.isNotEmpty) {
|
||||
parts.add(episodeTitle!);
|
||||
}
|
||||
return parts.join(' · ');
|
||||
}
|
||||
|
||||
|
||||
static TraktCalendarEntry? fromShowJson(Map<String, dynamic> json) {
|
||||
final aired = _parseDate(json['first_aired']);
|
||||
if (aired == null) return null;
|
||||
|
||||
final show = json['show'];
|
||||
final episode = json['episode'];
|
||||
if (show is! Map<String, dynamic>) return null;
|
||||
|
||||
return TraktCalendarEntry(
|
||||
firstAired: aired,
|
||||
type: 'show',
|
||||
showTitle: show['title'] as String?,
|
||||
showYear: (show['year'] as num?)?.toInt(),
|
||||
showIds: TraktIds.fromJson(show['ids']),
|
||||
episodeSeason: episode is Map<String, dynamic>
|
||||
? (episode['season'] as num?)?.toInt()
|
||||
: null,
|
||||
episodeNumber: episode is Map<String, dynamic>
|
||||
? (episode['number'] as num?)?.toInt()
|
||||
: null,
|
||||
episodeTitle: episode is Map<String, dynamic>
|
||||
? episode['title'] as String?
|
||||
: null,
|
||||
episodeIds: episode is Map<String, dynamic>
|
||||
? TraktIds.fromJson(episode['ids'])
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
static TraktCalendarEntry? fromMovieJson(Map<String, dynamic> json) {
|
||||
final releasedStr = json['released'] as String?;
|
||||
final aired = releasedStr != null && releasedStr.isNotEmpty
|
||||
? DateTime.tryParse(releasedStr)
|
||||
: null;
|
||||
if (aired == null) return null;
|
||||
|
||||
final movie = json['movie'];
|
||||
if (movie is! Map<String, dynamic>) return null;
|
||||
|
||||
return TraktCalendarEntry(
|
||||
firstAired: aired,
|
||||
type: 'movie',
|
||||
movieTitle: movie['title'] as String?,
|
||||
movieYear: (movie['year'] as num?)?.toInt(),
|
||||
movieIds: TraktIds.fromJson(movie['ids']),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DateTime? _parseDate(Object? raw) {
|
||||
if (raw is! String || raw.isEmpty) return null;
|
||||
return DateTime.tryParse(raw);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
|
||||
|
||||
class TraktIds {
|
||||
final int? trakt;
|
||||
final String? imdb;
|
||||
final int? tmdb;
|
||||
final int? tvdb;
|
||||
final String? slug;
|
||||
|
||||
const TraktIds({this.trakt, this.imdb, this.tmdb, this.tvdb, this.slug});
|
||||
|
||||
factory TraktIds.fromJson(Object? raw) {
|
||||
if (raw is! Map) return const TraktIds();
|
||||
final json = raw.cast<String, dynamic>();
|
||||
return TraktIds(
|
||||
trakt: (json['trakt'] as num?)?.toInt(),
|
||||
imdb: json['imdb'] as String?,
|
||||
tmdb: (json['tmdb'] as num?)?.toInt(),
|
||||
tvdb: (json['tvdb'] as num?)?.toInt(),
|
||||
slug: json['slug'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
bool get hasReliableId =>
|
||||
(trakt != null && trakt! > 0) ||
|
||||
(imdb != null && imdb!.isNotEmpty) ||
|
||||
(tmdb != null && tmdb! > 0) ||
|
||||
(tvdb != null && tvdb! > 0);
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final out = <String, dynamic>{};
|
||||
if (trakt != null && trakt! > 0) out['trakt'] = trakt;
|
||||
if (imdb != null && imdb!.isNotEmpty) out['imdb'] = imdb;
|
||||
if (tmdb != null && tmdb! > 0) out['tmdb'] = tmdb;
|
||||
if (tvdb != null && tvdb! > 0) out['tvdb'] = tvdb;
|
||||
if (slug != null && slug!.isNotEmpty) out['slug'] = slug;
|
||||
return out;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import 'trakt_ids.dart';
|
||||
|
||||
|
||||
class TraktPlaybackEntry {
|
||||
|
||||
final double progress;
|
||||
final DateTime? pausedAt;
|
||||
|
||||
|
||||
final String type;
|
||||
|
||||
final String? movieTitle;
|
||||
final int? movieYear;
|
||||
final TraktIds? movieIds;
|
||||
|
||||
final String? showTitle;
|
||||
final int? showYear;
|
||||
final TraktIds? showIds;
|
||||
|
||||
final int? episodeSeason;
|
||||
final int? episodeNumber;
|
||||
final String? episodeTitle;
|
||||
final TraktIds? episodeIds;
|
||||
|
||||
const TraktPlaybackEntry({
|
||||
required this.progress,
|
||||
required this.type,
|
||||
this.pausedAt,
|
||||
this.movieTitle,
|
||||
this.movieYear,
|
||||
this.movieIds,
|
||||
this.showTitle,
|
||||
this.showYear,
|
||||
this.showIds,
|
||||
this.episodeSeason,
|
||||
this.episodeNumber,
|
||||
this.episodeTitle,
|
||||
this.episodeIds,
|
||||
});
|
||||
|
||||
static TraktPlaybackEntry? fromJson(Map<String, dynamic> json) {
|
||||
final movie = json['movie'];
|
||||
final show = json['show'];
|
||||
final episode = json['episode'];
|
||||
final hasMovie = movie is Map<String, dynamic>;
|
||||
final hasShow = show is Map<String, dynamic>;
|
||||
if (!hasMovie && !hasShow) return null;
|
||||
|
||||
final rawType = json['type'] as String? ?? '';
|
||||
return TraktPlaybackEntry(
|
||||
progress: (json['progress'] as num?)?.toDouble() ?? 0,
|
||||
pausedAt: _parseDate(json['paused_at']),
|
||||
type: rawType.isNotEmpty ? rawType : (hasMovie ? 'movie' : 'episode'),
|
||||
movieTitle: hasMovie ? movie['title'] as String? : null,
|
||||
movieYear: hasMovie ? (movie['year'] as num?)?.toInt() : null,
|
||||
movieIds: hasMovie ? TraktIds.fromJson(movie['ids']) : null,
|
||||
showTitle: hasShow ? show['title'] as String? : null,
|
||||
showYear: hasShow ? (show['year'] as num?)?.toInt() : null,
|
||||
showIds: hasShow ? TraktIds.fromJson(show['ids']) : null,
|
||||
episodeSeason: episode is Map<String, dynamic>
|
||||
? (episode['season'] as num?)?.toInt()
|
||||
: null,
|
||||
episodeNumber: episode is Map<String, dynamic>
|
||||
? (episode['number'] as num?)?.toInt()
|
||||
: null,
|
||||
episodeTitle: episode is Map<String, dynamic>
|
||||
? episode['title'] as String?
|
||||
: null,
|
||||
episodeIds: episode is Map<String, dynamic>
|
||||
? TraktIds.fromJson(episode['ids'])
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DateTime? _parseDate(Object? raw) {
|
||||
if (raw is! String || raw.isEmpty) return null;
|
||||
return DateTime.tryParse(raw);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
|
||||
enum TraktResponseClass {
|
||||
|
||||
success,
|
||||
|
||||
|
||||
duplicate,
|
||||
|
||||
|
||||
invalid,
|
||||
|
||||
|
||||
rateLimited,
|
||||
|
||||
|
||||
unauthorized,
|
||||
|
||||
|
||||
transient,
|
||||
}
|
||||
|
||||
|
||||
class TraktScrobbleOutcome {
|
||||
final TraktResponseClass cls;
|
||||
|
||||
|
||||
final Duration? retryAfter;
|
||||
|
||||
const TraktScrobbleOutcome(this.cls, {this.retryAfter});
|
||||
}
|
||||
|
||||
|
||||
TraktResponseClass classifyTraktStatus(int statusCode) {
|
||||
if (statusCode >= 200 && statusCode < 300) return TraktResponseClass.success;
|
||||
switch (statusCode) {
|
||||
case 401:
|
||||
return TraktResponseClass.unauthorized;
|
||||
case 409:
|
||||
return TraktResponseClass.duplicate;
|
||||
case 422:
|
||||
return TraktResponseClass.invalid;
|
||||
case 429:
|
||||
return TraktResponseClass.rateLimited;
|
||||
default:
|
||||
return TraktResponseClass.transient;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import 'trakt_ids.dart';
|
||||
|
||||
|
||||
enum TraktMediaKind { movie, episode }
|
||||
|
||||
|
||||
class TraktScrobbleTarget {
|
||||
final TraktMediaKind kind;
|
||||
|
||||
final String title;
|
||||
final int? year;
|
||||
final TraktIds ids;
|
||||
|
||||
final int? season;
|
||||
final int? number;
|
||||
final TraktIds episodeIds;
|
||||
|
||||
const TraktScrobbleTarget._({
|
||||
required this.kind,
|
||||
required this.title,
|
||||
required this.year,
|
||||
required this.ids,
|
||||
this.season,
|
||||
this.number,
|
||||
this.episodeIds = const TraktIds(),
|
||||
});
|
||||
|
||||
factory TraktScrobbleTarget.movie({
|
||||
required String title,
|
||||
required int? year,
|
||||
required TraktIds ids,
|
||||
}) {
|
||||
return TraktScrobbleTarget._(
|
||||
kind: TraktMediaKind.movie,
|
||||
title: title,
|
||||
year: year,
|
||||
ids: ids,
|
||||
);
|
||||
}
|
||||
|
||||
factory TraktScrobbleTarget.episode({
|
||||
required String showTitle,
|
||||
required int? showYear,
|
||||
required TraktIds showIds,
|
||||
required int? season,
|
||||
required int? number,
|
||||
required TraktIds episodeIds,
|
||||
}) {
|
||||
return TraktScrobbleTarget._(
|
||||
kind: TraktMediaKind.episode,
|
||||
title: showTitle,
|
||||
year: showYear,
|
||||
ids: showIds,
|
||||
season: season,
|
||||
number: number,
|
||||
episodeIds: episodeIds,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Map<String, dynamic> toBody({
|
||||
required double progress,
|
||||
String? appVersion,
|
||||
String? appDate,
|
||||
}) {
|
||||
final body = <String, dynamic>{};
|
||||
|
||||
switch (kind) {
|
||||
case TraktMediaKind.movie:
|
||||
body['movie'] = _mediaNode(title, year, ids);
|
||||
case TraktMediaKind.episode:
|
||||
body['show'] = _mediaNode(title, year, ids);
|
||||
final episode = <String, dynamic>{'season': season, 'number': number};
|
||||
final epIds = episodeIds.toJson();
|
||||
if (epIds.isNotEmpty) episode['ids'] = epIds;
|
||||
body['episode'] = episode;
|
||||
}
|
||||
|
||||
body['progress'] = progress.toDouble();
|
||||
if (appVersion != null && appVersion.isNotEmpty) {
|
||||
body['app_version'] = appVersion;
|
||||
}
|
||||
if (appDate != null && appDate.isNotEmpty) body['app_date'] = appDate;
|
||||
return body;
|
||||
}
|
||||
|
||||
static Map<String, dynamic> _mediaNode(
|
||||
String title,
|
||||
int? year,
|
||||
TraktIds ids,
|
||||
) {
|
||||
final node = <String, dynamic>{'title': title};
|
||||
if (year != null && year > 0) node['year'] = year;
|
||||
node['ids'] = ids.toJson();
|
||||
return node;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
|
||||
enum TraktSyncJobType {
|
||||
scrobbleStart('scrobble_start'),
|
||||
scrobblePause('scrobble_pause'),
|
||||
scrobbleStop('scrobble_stop'),
|
||||
historyAdd('history_add'),
|
||||
historyRemove('history_remove');
|
||||
|
||||
const TraktSyncJobType(this.wire);
|
||||
|
||||
|
||||
final String wire;
|
||||
|
||||
bool get isHistory => this == historyAdd || this == historyRemove;
|
||||
|
||||
static TraktSyncJobType fromWire(String wire) {
|
||||
return TraktSyncJobType.values.firstWhere(
|
||||
(e) => e.wire == wire,
|
||||
orElse: () => TraktSyncJobType.scrobbleStart,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TraktSyncJob {
|
||||
final String id;
|
||||
final TraktSyncJobType type;
|
||||
final Map<String, dynamic> body;
|
||||
final int retryCount;
|
||||
final DateTime? nextRetryAt;
|
||||
final bool exhausted;
|
||||
|
||||
const TraktSyncJob({
|
||||
required this.id,
|
||||
required this.type,
|
||||
required this.body,
|
||||
this.retryCount = 0,
|
||||
this.nextRetryAt,
|
||||
this.exhausted = false,
|
||||
});
|
||||
|
||||
TraktSyncJob copyWith({
|
||||
int? retryCount,
|
||||
DateTime? nextRetryAt,
|
||||
bool clearNextRetryAt = false,
|
||||
bool? exhausted,
|
||||
}) {
|
||||
return TraktSyncJob(
|
||||
id: id,
|
||||
type: type,
|
||||
body: body,
|
||||
retryCount: retryCount ?? this.retryCount,
|
||||
nextRetryAt: clearNextRetryAt ? null : (nextRetryAt ?? this.nextRetryAt),
|
||||
exhausted: exhausted ?? this.exhausted,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'type': type.wire,
|
||||
'body': body,
|
||||
'retryCount': retryCount,
|
||||
'nextRetryAt': nextRetryAt?.toIso8601String(),
|
||||
'exhausted': exhausted,
|
||||
};
|
||||
|
||||
factory TraktSyncJob.fromJson(Map<String, dynamic> json) {
|
||||
return TraktSyncJob(
|
||||
id: json['id'] as String,
|
||||
type: TraktSyncJobType.fromWire(json['type'] as String? ?? ''),
|
||||
body: (json['body'] as Map?)?.cast<String, dynamic>() ?? const {},
|
||||
retryCount: (json['retryCount'] as num?)?.toInt() ?? 0,
|
||||
nextRetryAt: _parseDate(json['nextRetryAt']),
|
||||
exhausted: json['exhausted'] as bool? ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
static DateTime? _parseDate(Object? raw) {
|
||||
if (raw is! String || raw.isEmpty) return null;
|
||||
return DateTime.tryParse(raw);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'trakt_token.freezed.dart';
|
||||
part 'trakt_token.g.dart';
|
||||
|
||||
|
||||
@freezed
|
||||
abstract class TraktToken with _$TraktToken {
|
||||
const factory TraktToken({
|
||||
@JsonKey(name: 'access_token') @Default('') String accessToken,
|
||||
@JsonKey(name: 'refresh_token') @Default('') String refreshToken,
|
||||
@JsonKey(name: 'expires_in') @Default(0) int expiresIn,
|
||||
@JsonKey(name: 'created_at') @Default(0) int createdAt,
|
||||
@JsonKey(name: 'token_type') @Default('') String tokenType,
|
||||
@Default('') String scope,
|
||||
}) = _TraktToken;
|
||||
|
||||
factory TraktToken.fromJson(Map<String, dynamic> json) =>
|
||||
_$TraktTokenFromJson(json);
|
||||
}
|
||||
+292
@@ -0,0 +1,292 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'trakt_token.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$TraktToken {
|
||||
|
||||
@JsonKey(name: 'access_token') String get accessToken;@JsonKey(name: 'refresh_token') String get refreshToken;@JsonKey(name: 'expires_in') int get expiresIn;@JsonKey(name: 'created_at') int get createdAt;@JsonKey(name: 'token_type') String get tokenType; String get scope;
|
||||
/// Create a copy of TraktToken
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$TraktTokenCopyWith<TraktToken> get copyWith => _$TraktTokenCopyWithImpl<TraktToken>(this as TraktToken, _$identity);
|
||||
|
||||
/// Serializes this TraktToken to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is TraktToken&&(identical(other.accessToken, accessToken) || other.accessToken == accessToken)&&(identical(other.refreshToken, refreshToken) || other.refreshToken == refreshToken)&&(identical(other.expiresIn, expiresIn) || other.expiresIn == expiresIn)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.tokenType, tokenType) || other.tokenType == tokenType)&&(identical(other.scope, scope) || other.scope == scope));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,accessToken,refreshToken,expiresIn,createdAt,tokenType,scope);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'TraktToken(accessToken: $accessToken, refreshToken: $refreshToken, expiresIn: $expiresIn, createdAt: $createdAt, tokenType: $tokenType, scope: $scope)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $TraktTokenCopyWith<$Res> {
|
||||
factory $TraktTokenCopyWith(TraktToken value, $Res Function(TraktToken) _then) = _$TraktTokenCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
@JsonKey(name: 'access_token') String accessToken,@JsonKey(name: 'refresh_token') String refreshToken,@JsonKey(name: 'expires_in') int expiresIn,@JsonKey(name: 'created_at') int createdAt,@JsonKey(name: 'token_type') String tokenType, String scope
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$TraktTokenCopyWithImpl<$Res>
|
||||
implements $TraktTokenCopyWith<$Res> {
|
||||
_$TraktTokenCopyWithImpl(this._self, this._then);
|
||||
|
||||
final TraktToken _self;
|
||||
final $Res Function(TraktToken) _then;
|
||||
|
||||
/// Create a copy of TraktToken
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? accessToken = null,Object? refreshToken = null,Object? expiresIn = null,Object? createdAt = null,Object? tokenType = null,Object? scope = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
accessToken: null == accessToken ? _self.accessToken : accessToken // ignore: cast_nullable_to_non_nullable
|
||||
as String,refreshToken: null == refreshToken ? _self.refreshToken : refreshToken // ignore: cast_nullable_to_non_nullable
|
||||
as String,expiresIn: null == expiresIn ? _self.expiresIn : expiresIn // ignore: cast_nullable_to_non_nullable
|
||||
as int,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as int,tokenType: null == tokenType ? _self.tokenType : tokenType // ignore: cast_nullable_to_non_nullable
|
||||
as String,scope: null == scope ? _self.scope : scope // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [TraktToken].
|
||||
extension TraktTokenPatterns on TraktToken {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _TraktToken value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _TraktToken() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _TraktToken value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _TraktToken():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _TraktToken value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _TraktToken() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function(@JsonKey(name: 'access_token') String accessToken, @JsonKey(name: 'refresh_token') String refreshToken, @JsonKey(name: 'expires_in') int expiresIn, @JsonKey(name: 'created_at') int createdAt, @JsonKey(name: 'token_type') String tokenType, String scope)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _TraktToken() when $default != null:
|
||||
return $default(_that.accessToken,_that.refreshToken,_that.expiresIn,_that.createdAt,_that.tokenType,_that.scope);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function(@JsonKey(name: 'access_token') String accessToken, @JsonKey(name: 'refresh_token') String refreshToken, @JsonKey(name: 'expires_in') int expiresIn, @JsonKey(name: 'created_at') int createdAt, @JsonKey(name: 'token_type') String tokenType, String scope) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _TraktToken():
|
||||
return $default(_that.accessToken,_that.refreshToken,_that.expiresIn,_that.createdAt,_that.tokenType,_that.scope);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function(@JsonKey(name: 'access_token') String accessToken, @JsonKey(name: 'refresh_token') String refreshToken, @JsonKey(name: 'expires_in') int expiresIn, @JsonKey(name: 'created_at') int createdAt, @JsonKey(name: 'token_type') String tokenType, String scope)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _TraktToken() when $default != null:
|
||||
return $default(_that.accessToken,_that.refreshToken,_that.expiresIn,_that.createdAt,_that.tokenType,_that.scope);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _TraktToken implements TraktToken {
|
||||
const _TraktToken({@JsonKey(name: 'access_token') this.accessToken = '', @JsonKey(name: 'refresh_token') this.refreshToken = '', @JsonKey(name: 'expires_in') this.expiresIn = 0, @JsonKey(name: 'created_at') this.createdAt = 0, @JsonKey(name: 'token_type') this.tokenType = '', this.scope = ''});
|
||||
factory _TraktToken.fromJson(Map<String, dynamic> json) => _$TraktTokenFromJson(json);
|
||||
|
||||
@override@JsonKey(name: 'access_token') final String accessToken;
|
||||
@override@JsonKey(name: 'refresh_token') final String refreshToken;
|
||||
@override@JsonKey(name: 'expires_in') final int expiresIn;
|
||||
@override@JsonKey(name: 'created_at') final int createdAt;
|
||||
@override@JsonKey(name: 'token_type') final String tokenType;
|
||||
@override@JsonKey() final String scope;
|
||||
|
||||
/// Create a copy of TraktToken
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$TraktTokenCopyWith<_TraktToken> get copyWith => __$TraktTokenCopyWithImpl<_TraktToken>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$TraktTokenToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _TraktToken&&(identical(other.accessToken, accessToken) || other.accessToken == accessToken)&&(identical(other.refreshToken, refreshToken) || other.refreshToken == refreshToken)&&(identical(other.expiresIn, expiresIn) || other.expiresIn == expiresIn)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.tokenType, tokenType) || other.tokenType == tokenType)&&(identical(other.scope, scope) || other.scope == scope));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,accessToken,refreshToken,expiresIn,createdAt,tokenType,scope);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'TraktToken(accessToken: $accessToken, refreshToken: $refreshToken, expiresIn: $expiresIn, createdAt: $createdAt, tokenType: $tokenType, scope: $scope)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$TraktTokenCopyWith<$Res> implements $TraktTokenCopyWith<$Res> {
|
||||
factory _$TraktTokenCopyWith(_TraktToken value, $Res Function(_TraktToken) _then) = __$TraktTokenCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
@JsonKey(name: 'access_token') String accessToken,@JsonKey(name: 'refresh_token') String refreshToken,@JsonKey(name: 'expires_in') int expiresIn,@JsonKey(name: 'created_at') int createdAt,@JsonKey(name: 'token_type') String tokenType, String scope
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$TraktTokenCopyWithImpl<$Res>
|
||||
implements _$TraktTokenCopyWith<$Res> {
|
||||
__$TraktTokenCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _TraktToken _self;
|
||||
final $Res Function(_TraktToken) _then;
|
||||
|
||||
/// Create a copy of TraktToken
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? accessToken = null,Object? refreshToken = null,Object? expiresIn = null,Object? createdAt = null,Object? tokenType = null,Object? scope = null,}) {
|
||||
return _then(_TraktToken(
|
||||
accessToken: null == accessToken ? _self.accessToken : accessToken // ignore: cast_nullable_to_non_nullable
|
||||
as String,refreshToken: null == refreshToken ? _self.refreshToken : refreshToken // ignore: cast_nullable_to_non_nullable
|
||||
as String,expiresIn: null == expiresIn ? _self.expiresIn : expiresIn // ignore: cast_nullable_to_non_nullable
|
||||
as int,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as int,tokenType: null == tokenType ? _self.tokenType : tokenType // ignore: cast_nullable_to_non_nullable
|
||||
as String,scope: null == scope ? _self.scope : scope // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'trakt_token.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_TraktToken _$TraktTokenFromJson(Map<String, dynamic> json) => _TraktToken(
|
||||
accessToken: json['access_token'] as String? ?? '',
|
||||
refreshToken: json['refresh_token'] as String? ?? '',
|
||||
expiresIn: (json['expires_in'] as num?)?.toInt() ?? 0,
|
||||
createdAt: (json['created_at'] as num?)?.toInt() ?? 0,
|
||||
tokenType: json['token_type'] as String? ?? '',
|
||||
scope: json['scope'] as String? ?? '',
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$TraktTokenToJson(_TraktToken instance) =>
|
||||
<String, dynamic>{
|
||||
'access_token': instance.accessToken,
|
||||
'refresh_token': instance.refreshToken,
|
||||
'expires_in': instance.expiresIn,
|
||||
'created_at': instance.createdAt,
|
||||
'token_type': instance.tokenType,
|
||||
'scope': instance.scope,
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'trakt_user.freezed.dart';
|
||||
part 'trakt_user.g.dart';
|
||||
|
||||
|
||||
@freezed
|
||||
abstract class TraktUser with _$TraktUser {
|
||||
const factory TraktUser({@Default('') String username}) = _TraktUser;
|
||||
|
||||
factory TraktUser.fromJson(Map<String, dynamic> json) =>
|
||||
_$TraktUserFromJson(json);
|
||||
}
|
||||
|
||||
|
||||
@freezed
|
||||
abstract class TraktUserSettings with _$TraktUserSettings {
|
||||
const factory TraktUserSettings({TraktUser? user}) = _TraktUserSettings;
|
||||
|
||||
factory TraktUserSettings.fromJson(Map<String, dynamic> json) =>
|
||||
_$TraktUserSettingsFromJson(json);
|
||||
}
|
||||
+564
@@ -0,0 +1,564 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'trakt_user.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$TraktUser {
|
||||
|
||||
String get username;
|
||||
/// Create a copy of TraktUser
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$TraktUserCopyWith<TraktUser> get copyWith => _$TraktUserCopyWithImpl<TraktUser>(this as TraktUser, _$identity);
|
||||
|
||||
/// Serializes this TraktUser to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is TraktUser&&(identical(other.username, username) || other.username == username));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,username);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'TraktUser(username: $username)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $TraktUserCopyWith<$Res> {
|
||||
factory $TraktUserCopyWith(TraktUser value, $Res Function(TraktUser) _then) = _$TraktUserCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String username
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$TraktUserCopyWithImpl<$Res>
|
||||
implements $TraktUserCopyWith<$Res> {
|
||||
_$TraktUserCopyWithImpl(this._self, this._then);
|
||||
|
||||
final TraktUser _self;
|
||||
final $Res Function(TraktUser) _then;
|
||||
|
||||
/// Create a copy of TraktUser
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? username = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
username: null == username ? _self.username : username // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [TraktUser].
|
||||
extension TraktUserPatterns on TraktUser {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _TraktUser value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _TraktUser() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _TraktUser value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _TraktUser():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _TraktUser value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _TraktUser() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String username)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _TraktUser() when $default != null:
|
||||
return $default(_that.username);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String username) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _TraktUser():
|
||||
return $default(_that.username);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String username)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _TraktUser() when $default != null:
|
||||
return $default(_that.username);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _TraktUser implements TraktUser {
|
||||
const _TraktUser({this.username = ''});
|
||||
factory _TraktUser.fromJson(Map<String, dynamic> json) => _$TraktUserFromJson(json);
|
||||
|
||||
@override@JsonKey() final String username;
|
||||
|
||||
/// Create a copy of TraktUser
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$TraktUserCopyWith<_TraktUser> get copyWith => __$TraktUserCopyWithImpl<_TraktUser>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$TraktUserToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _TraktUser&&(identical(other.username, username) || other.username == username));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,username);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'TraktUser(username: $username)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$TraktUserCopyWith<$Res> implements $TraktUserCopyWith<$Res> {
|
||||
factory _$TraktUserCopyWith(_TraktUser value, $Res Function(_TraktUser) _then) = __$TraktUserCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String username
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$TraktUserCopyWithImpl<$Res>
|
||||
implements _$TraktUserCopyWith<$Res> {
|
||||
__$TraktUserCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _TraktUser _self;
|
||||
final $Res Function(_TraktUser) _then;
|
||||
|
||||
/// Create a copy of TraktUser
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? username = null,}) {
|
||||
return _then(_TraktUser(
|
||||
username: null == username ? _self.username : username // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// @nodoc
|
||||
mixin _$TraktUserSettings {
|
||||
|
||||
TraktUser? get user;
|
||||
/// Create a copy of TraktUserSettings
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$TraktUserSettingsCopyWith<TraktUserSettings> get copyWith => _$TraktUserSettingsCopyWithImpl<TraktUserSettings>(this as TraktUserSettings, _$identity);
|
||||
|
||||
/// Serializes this TraktUserSettings to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is TraktUserSettings&&(identical(other.user, user) || other.user == user));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,user);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'TraktUserSettings(user: $user)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $TraktUserSettingsCopyWith<$Res> {
|
||||
factory $TraktUserSettingsCopyWith(TraktUserSettings value, $Res Function(TraktUserSettings) _then) = _$TraktUserSettingsCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
TraktUser? user
|
||||
});
|
||||
|
||||
|
||||
$TraktUserCopyWith<$Res>? get user;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$TraktUserSettingsCopyWithImpl<$Res>
|
||||
implements $TraktUserSettingsCopyWith<$Res> {
|
||||
_$TraktUserSettingsCopyWithImpl(this._self, this._then);
|
||||
|
||||
final TraktUserSettings _self;
|
||||
final $Res Function(TraktUserSettings) _then;
|
||||
|
||||
/// Create a copy of TraktUserSettings
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? user = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
user: freezed == user ? _self.user : user // ignore: cast_nullable_to_non_nullable
|
||||
as TraktUser?,
|
||||
));
|
||||
}
|
||||
/// Create a copy of TraktUserSettings
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$TraktUserCopyWith<$Res>? get user {
|
||||
if (_self.user == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $TraktUserCopyWith<$Res>(_self.user!, (value) {
|
||||
return _then(_self.copyWith(user: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [TraktUserSettings].
|
||||
extension TraktUserSettingsPatterns on TraktUserSettings {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _TraktUserSettings value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _TraktUserSettings() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _TraktUserSettings value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _TraktUserSettings():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _TraktUserSettings value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _TraktUserSettings() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( TraktUser? user)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _TraktUserSettings() when $default != null:
|
||||
return $default(_that.user);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( TraktUser? user) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _TraktUserSettings():
|
||||
return $default(_that.user);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( TraktUser? user)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _TraktUserSettings() when $default != null:
|
||||
return $default(_that.user);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _TraktUserSettings implements TraktUserSettings {
|
||||
const _TraktUserSettings({this.user});
|
||||
factory _TraktUserSettings.fromJson(Map<String, dynamic> json) => _$TraktUserSettingsFromJson(json);
|
||||
|
||||
@override final TraktUser? user;
|
||||
|
||||
/// Create a copy of TraktUserSettings
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$TraktUserSettingsCopyWith<_TraktUserSettings> get copyWith => __$TraktUserSettingsCopyWithImpl<_TraktUserSettings>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$TraktUserSettingsToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _TraktUserSettings&&(identical(other.user, user) || other.user == user));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,user);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'TraktUserSettings(user: $user)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$TraktUserSettingsCopyWith<$Res> implements $TraktUserSettingsCopyWith<$Res> {
|
||||
factory _$TraktUserSettingsCopyWith(_TraktUserSettings value, $Res Function(_TraktUserSettings) _then) = __$TraktUserSettingsCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
TraktUser? user
|
||||
});
|
||||
|
||||
|
||||
@override $TraktUserCopyWith<$Res>? get user;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$TraktUserSettingsCopyWithImpl<$Res>
|
||||
implements _$TraktUserSettingsCopyWith<$Res> {
|
||||
__$TraktUserSettingsCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _TraktUserSettings _self;
|
||||
final $Res Function(_TraktUserSettings) _then;
|
||||
|
||||
/// Create a copy of TraktUserSettings
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? user = freezed,}) {
|
||||
return _then(_TraktUserSettings(
|
||||
user: freezed == user ? _self.user : user // ignore: cast_nullable_to_non_nullable
|
||||
as TraktUser?,
|
||||
));
|
||||
}
|
||||
|
||||
/// Create a copy of TraktUserSettings
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$TraktUserCopyWith<$Res>? get user {
|
||||
if (_self.user == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $TraktUserCopyWith<$Res>(_self.user!, (value) {
|
||||
return _then(_self.copyWith(user: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// dart format on
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'trakt_user.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_TraktUser _$TraktUserFromJson(Map<String, dynamic> json) =>
|
||||
_TraktUser(username: json['username'] as String? ?? '');
|
||||
|
||||
Map<String, dynamic> _$TraktUserToJson(_TraktUser instance) =>
|
||||
<String, dynamic>{'username': instance.username};
|
||||
|
||||
_TraktUserSettings _$TraktUserSettingsFromJson(Map<String, dynamic> json) =>
|
||||
_TraktUserSettings(
|
||||
user: json['user'] == null
|
||||
? null
|
||||
: TraktUser.fromJson(json['user'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$TraktUserSettingsToJson(_TraktUserSettings instance) =>
|
||||
<String, dynamic>{'user': instance.user};
|
||||
@@ -0,0 +1,13 @@
|
||||
import '../contracts/error.dart';
|
||||
|
||||
class DomainError implements Exception {
|
||||
final ErrorCode code;
|
||||
final String message;
|
||||
final bool retryable;
|
||||
final Object? details;
|
||||
|
||||
DomainError(this.code, this.message, {this.retryable = false, this.details});
|
||||
|
||||
@override
|
||||
String toString() => 'DomainError(${code.value}): $message';
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import '../../contracts/trakt/trakt_response.dart';
|
||||
import '../../contracts/trakt/trakt_sync_job.dart';
|
||||
|
||||
|
||||
abstract class AuthedTraktGateway {
|
||||
|
||||
Future<bool> isConnected();
|
||||
|
||||
|
||||
Future<TraktScrobbleOutcome> scrobble(
|
||||
TraktSyncJobType type,
|
||||
Map<String, dynamic> body,
|
||||
);
|
||||
|
||||
|
||||
Future<TraktScrobbleOutcome> syncHistory(
|
||||
TraktSyncJobType type,
|
||||
Map<String, dynamic> body,
|
||||
);
|
||||
|
||||
|
||||
Future<List<Map<String, dynamic>>> fetchPlayback();
|
||||
|
||||
|
||||
Future<List<Map<String, dynamic>>> fetchCalendarShows();
|
||||
|
||||
|
||||
Future<List<Map<String, dynamic>>> fetchCalendarMovies();
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import '../../contracts/danmaku.dart';
|
||||
|
||||
abstract class DanmakuGateway {
|
||||
|
||||
|
||||
Future<List<DanmakuMatch>> match(
|
||||
String baseUrl,
|
||||
String fileName,
|
||||
int durationSec,
|
||||
);
|
||||
|
||||
|
||||
Future<List<DanmakuMatch>> searchEpisodes(
|
||||
String baseUrl,
|
||||
String anime,
|
||||
int? episode,
|
||||
);
|
||||
|
||||
|
||||
Future<List<DanmakuComment>> fetchComments(
|
||||
String baseUrl,
|
||||
int episodeId, {
|
||||
int chConvert = 0,
|
||||
});
|
||||
|
||||
|
||||
Future<List<DanmakuBangumiEpisode>> getBangumiEpisodes(
|
||||
String baseUrl,
|
||||
int animeId,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import '../../contracts/danmaku.dart';
|
||||
|
||||
|
||||
abstract class DanmakuSourcesStore {
|
||||
|
||||
Future<List<DanmakuSource>> list();
|
||||
|
||||
|
||||
Future<void> replaceAll(List<DanmakuSource> sources);
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
import '../../contracts/cancellation.dart';
|
||||
import '../../contracts/library.dart';
|
||||
import '../../contracts/playback.dart';
|
||||
import '../../contracts/server.dart';
|
||||
|
||||
|
||||
class AuthedRequestContext {
|
||||
final String baseUrl;
|
||||
final String token;
|
||||
final String userId;
|
||||
|
||||
const AuthedRequestContext({
|
||||
required this.baseUrl,
|
||||
required this.token,
|
||||
required this.userId,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
class EmbyAuthResult {
|
||||
final String accessToken;
|
||||
final String userId;
|
||||
final String userName;
|
||||
|
||||
const EmbyAuthResult({
|
||||
required this.accessToken,
|
||||
required this.userId,
|
||||
required this.userName,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
abstract class EmbyGateway {
|
||||
Future<ProbePublicInfoRes> probePublicInfo(String baseUrl);
|
||||
|
||||
Future<EmbyAuthResult> authenticateByName({
|
||||
required String baseUrl,
|
||||
required String username,
|
||||
required String password,
|
||||
});
|
||||
|
||||
Future<void> changePassword({
|
||||
required AuthedRequestContext ctx,
|
||||
required String currentPassword,
|
||||
required String newPassword,
|
||||
});
|
||||
|
||||
Future<EmbyRawLibraries> getUserViews(AuthedRequestContext ctx);
|
||||
|
||||
Future<LibraryLatestRes> getLatestItems(
|
||||
AuthedRequestContext ctx,
|
||||
String parentId, {
|
||||
int? limit,
|
||||
CancellationToken? cancelToken,
|
||||
});
|
||||
|
||||
Future<LibraryResumeRes> getResumeItems(
|
||||
AuthedRequestContext ctx, {
|
||||
int? limit,
|
||||
});
|
||||
|
||||
Future<List<EmbyRawItem>> getLibraryItems({
|
||||
required AuthedRequestContext ctx,
|
||||
String? parentId,
|
||||
String? includeItemTypes,
|
||||
List<String>? genreIds,
|
||||
String? searchTerm,
|
||||
String? anyProviderIdEquals,
|
||||
List<String>? personIds,
|
||||
String? fields,
|
||||
String? enableImageTypes,
|
||||
bool? groupProgramsBySeries,
|
||||
int? imageTypeLimit,
|
||||
int? startIndex,
|
||||
int? limit,
|
||||
bool? recursive,
|
||||
String? sortBy,
|
||||
SortOrder? sortOrder,
|
||||
String? filters,
|
||||
CancellationToken? cancelToken,
|
||||
});
|
||||
|
||||
Future<List<EmbyRawItem>> getFavoriteItems({
|
||||
required AuthedRequestContext ctx,
|
||||
required String includeItemTypes,
|
||||
required String sortBy,
|
||||
SortOrder? sortOrder,
|
||||
});
|
||||
|
||||
Future<ItemCountsRes> getItemCounts(AuthedRequestContext ctx);
|
||||
|
||||
Future<EmbyRawItem> getItemDetail(AuthedRequestContext ctx, String itemId);
|
||||
|
||||
Future<List<EmbyRawItem>> getSeriesNextUp(
|
||||
AuthedRequestContext ctx,
|
||||
String seriesId, {
|
||||
int? limit,
|
||||
});
|
||||
|
||||
Future<List<EmbyRawSeason>> getSeriesSeasons(
|
||||
AuthedRequestContext ctx,
|
||||
String seriesId, {
|
||||
CancellationToken? cancelToken,
|
||||
});
|
||||
|
||||
Future<List<EmbyRawItem>> getSeriesEpisodes(
|
||||
AuthedRequestContext ctx,
|
||||
String seriesId,
|
||||
String seasonId, {
|
||||
CancellationToken? cancelToken,
|
||||
});
|
||||
|
||||
Future<List<EmbyRawItem>> getSimilarItems(
|
||||
AuthedRequestContext ctx,
|
||||
String itemId, {
|
||||
int? limit,
|
||||
});
|
||||
|
||||
Future<List<EmbyRawItem>> getAdditionalParts(
|
||||
AuthedRequestContext ctx,
|
||||
String itemId,
|
||||
);
|
||||
|
||||
Future<List<EmbyRawItem>> getSpecialFeatures(
|
||||
AuthedRequestContext ctx,
|
||||
String itemId,
|
||||
);
|
||||
|
||||
Future<bool> setFavoriteStatus({
|
||||
required AuthedRequestContext ctx,
|
||||
required String itemId,
|
||||
required bool isFavorite,
|
||||
});
|
||||
|
||||
Future<bool> setPlayedStatus({
|
||||
required AuthedRequestContext ctx,
|
||||
required String itemId,
|
||||
required bool played,
|
||||
});
|
||||
|
||||
Future<bool> hideFromResume({
|
||||
required AuthedRequestContext ctx,
|
||||
required String itemId,
|
||||
required bool hide,
|
||||
});
|
||||
|
||||
Future<void> reportPlaybackStarted({
|
||||
required AuthedRequestContext ctx,
|
||||
required PlaybackReportPayload payload,
|
||||
});
|
||||
|
||||
Future<void> reportPlaybackProgress({
|
||||
required AuthedRequestContext ctx,
|
||||
required PlaybackReportPayload payload,
|
||||
});
|
||||
|
||||
Future<void> reportPlaybackStopped({
|
||||
required AuthedRequestContext ctx,
|
||||
required PlaybackReportPayload payload,
|
||||
});
|
||||
|
||||
|
||||
Future<Map<String, dynamic>> fetchPlaybackInfo({
|
||||
required AuthedRequestContext ctx,
|
||||
required String itemId,
|
||||
String? mediaSourceId,
|
||||
int startTimeTicks = 0,
|
||||
bool isPlayback = false,
|
||||
required Map<String, dynamic> deviceProfile,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
abstract class ScriptWidgetRemoteGateway {
|
||||
Future<String> fetchText(String url);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import '../../contracts/script_widget.dart';
|
||||
|
||||
abstract class ScriptWidgetRepository {
|
||||
Future<List<InstalledScriptWidget>> listWidgets();
|
||||
|
||||
Future<InstalledScriptWidget?> findWidget(String widgetId);
|
||||
|
||||
Future<void> upsertWidget(InstalledScriptWidget widget);
|
||||
|
||||
Future<void> deleteWidget(String widgetId);
|
||||
|
||||
Future<List<ScriptWidgetSubscription>> listSubscriptions();
|
||||
|
||||
Future<void> upsertSubscription(ScriptWidgetSubscription subscription);
|
||||
|
||||
|
||||
Future<void> replaceAllWidgets(List<InstalledScriptWidget> widgets);
|
||||
|
||||
|
||||
Future<void> replaceAllSubscriptions(
|
||||
List<ScriptWidgetSubscription> subscriptions,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import '../../contracts/script_widget.dart';
|
||||
|
||||
abstract class ScriptWidgetRuntime {
|
||||
Future<ScriptWidgetManifest> inspectWidget(String scriptSource);
|
||||
|
||||
Future<ScriptWidgetManifest> loadWidget(String scriptSource);
|
||||
|
||||
bool isWidgetLoaded(String widgetId);
|
||||
|
||||
Future<Object?> invokeModuleFunction(
|
||||
String widgetId,
|
||||
String functionName,
|
||||
Map<String, Object?> params,
|
||||
);
|
||||
|
||||
Future<void> unloadWidget(String widgetId);
|
||||
|
||||
Future<void> dispose();
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import '../../contracts/server.dart';
|
||||
|
||||
abstract class ServerRepository {
|
||||
Future<List<EmbyServer>> list();
|
||||
Future<EmbyServer?> findById(String id);
|
||||
Future<EmbyServer?> findByBaseUrl(String baseUrl);
|
||||
Future<EmbyServer> create(EmbyServer server);
|
||||
Future<EmbyServer> update(EmbyServer server);
|
||||
Future<void> deleteById(String id);
|
||||
Future<void> touchConnectedAt(String id, String updatedAt);
|
||||
|
||||
|
||||
Future<void> replaceAll(List<EmbyServer> servers);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
|
||||
|
||||
library;
|
||||
|
||||
|
||||
class ServerSyncSnapshot {
|
||||
final String? blob;
|
||||
|
||||
const ServerSyncSnapshot({this.blob});
|
||||
|
||||
bool get isEmpty => blob == null || blob!.trim().isEmpty;
|
||||
}
|
||||
|
||||
abstract class ServerSyncGateway {
|
||||
|
||||
|
||||
Future<ServerSyncSnapshot> pull({
|
||||
required String baseUrl,
|
||||
required String accessToken,
|
||||
});
|
||||
|
||||
|
||||
Future<void> push({
|
||||
required String baseUrl,
|
||||
required String accessToken,
|
||||
required String blob,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import '../../contracts/auth.dart';
|
||||
|
||||
abstract class SessionRepository {
|
||||
Future<SessionData> set(SessionData session);
|
||||
|
||||
|
||||
Future<void> setMany(List<SessionData> sessions);
|
||||
|
||||
|
||||
Future<void> replaceAll(List<SessionData> sessions, {String? activeServerId});
|
||||
|
||||
Future<SessionData?> getByServerId(String serverId);
|
||||
Future<SessionData?> getActive();
|
||||
Future<String?> getActiveServerId();
|
||||
Future<List<SessionData>> listAll();
|
||||
Future<void> setActive(String serverId);
|
||||
Future<void> clear([String? serverId]);
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
|
||||
|
||||
library;
|
||||
|
||||
|
||||
class SmLoginResult {
|
||||
final bool ok;
|
||||
final String email;
|
||||
|
||||
|
||||
final String accessToken;
|
||||
|
||||
|
||||
final String refreshToken;
|
||||
|
||||
final int expiresIn;
|
||||
|
||||
|
||||
final bool isVip;
|
||||
|
||||
|
||||
final int? vipExpiresAt;
|
||||
|
||||
|
||||
final bool vipPermanent;
|
||||
|
||||
|
||||
final bool needsVerification;
|
||||
|
||||
final String? message;
|
||||
|
||||
const SmLoginResult({
|
||||
required this.ok,
|
||||
this.email = '',
|
||||
this.accessToken = '',
|
||||
this.refreshToken = '',
|
||||
this.expiresIn = 0,
|
||||
this.isVip = false,
|
||||
this.vipExpiresAt,
|
||||
this.vipPermanent = false,
|
||||
this.needsVerification = false,
|
||||
this.message,
|
||||
});
|
||||
|
||||
factory SmLoginResult.failure(
|
||||
String message, {
|
||||
bool needsVerification = false,
|
||||
}) => SmLoginResult(
|
||||
ok: false,
|
||||
needsVerification: needsVerification,
|
||||
message: message,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
class SmTokenRefreshResult {
|
||||
final bool ok;
|
||||
final String accessToken;
|
||||
final String refreshToken;
|
||||
final int expiresIn;
|
||||
|
||||
|
||||
final bool isVip;
|
||||
final int? vipExpiresAt;
|
||||
final bool vipPermanent;
|
||||
|
||||
|
||||
final bool hasVipInfo;
|
||||
|
||||
|
||||
final bool invalidRefreshToken;
|
||||
final String? message;
|
||||
|
||||
const SmTokenRefreshResult({
|
||||
required this.ok,
|
||||
this.accessToken = '',
|
||||
this.refreshToken = '',
|
||||
this.expiresIn = 0,
|
||||
this.isVip = false,
|
||||
this.vipExpiresAt,
|
||||
this.vipPermanent = false,
|
||||
this.hasVipInfo = false,
|
||||
this.invalidRefreshToken = false,
|
||||
this.message,
|
||||
});
|
||||
|
||||
factory SmTokenRefreshResult.failure(
|
||||
String message, {
|
||||
bool invalidRefreshToken = false,
|
||||
}) => SmTokenRefreshResult(
|
||||
ok: false,
|
||||
invalidRefreshToken: invalidRefreshToken,
|
||||
message: message,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
class SmActionResult {
|
||||
final bool ok;
|
||||
final String? message;
|
||||
|
||||
const SmActionResult({required this.ok, this.message});
|
||||
|
||||
factory SmActionResult.success() => const SmActionResult(ok: true);
|
||||
|
||||
factory SmActionResult.failure(String message) =>
|
||||
SmActionResult(ok: false, message: message);
|
||||
}
|
||||
|
||||
|
||||
abstract class SmAccountGateway {
|
||||
|
||||
|
||||
Future<SmLoginResult> login({
|
||||
required String baseUrl,
|
||||
required String email,
|
||||
required String password,
|
||||
});
|
||||
|
||||
|
||||
Future<SmActionResult> registerStart({
|
||||
required String baseUrl,
|
||||
required String email,
|
||||
});
|
||||
|
||||
|
||||
Future<SmLoginResult> registerVerify({
|
||||
required String baseUrl,
|
||||
required String email,
|
||||
required String password,
|
||||
required String code,
|
||||
});
|
||||
|
||||
|
||||
Future<SmActionResult> resendCode({
|
||||
required String baseUrl,
|
||||
required String email,
|
||||
});
|
||||
|
||||
|
||||
Future<void> logout({required String baseUrl, required String refreshToken});
|
||||
|
||||
|
||||
Future<SmTokenRefreshResult> refreshAccessToken({
|
||||
required String baseUrl,
|
||||
required String refreshToken,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import '../../contracts/tmdb.dart';
|
||||
|
||||
|
||||
class TmdbRequestConfig {
|
||||
final String apiBaseUrl;
|
||||
final String imageBaseUrl;
|
||||
final String accessToken;
|
||||
final String apiKey;
|
||||
|
||||
|
||||
final Future<String?> Function()? onUnauthorized;
|
||||
|
||||
const TmdbRequestConfig({
|
||||
required this.apiBaseUrl,
|
||||
required this.imageBaseUrl,
|
||||
this.accessToken = '',
|
||||
this.apiKey = '',
|
||||
this.onUnauthorized,
|
||||
});
|
||||
}
|
||||
|
||||
abstract class TmdbGateway {
|
||||
Future<TmdbCreditsRes?> getCredits(
|
||||
TmdbRequestConfig config,
|
||||
String tmdbId,
|
||||
String mediaType, {
|
||||
required String language,
|
||||
});
|
||||
|
||||
Future<TmdbRecommendationsRes?> getRecommendations(
|
||||
TmdbRequestConfig config,
|
||||
String tmdbId,
|
||||
String mediaType, {
|
||||
required String language,
|
||||
});
|
||||
|
||||
Future<TmdbSeasonDetail?> getSeasonDetail(
|
||||
TmdbRequestConfig config,
|
||||
String tmdbId,
|
||||
int seasonNumber, {
|
||||
required String language,
|
||||
});
|
||||
|
||||
Future<TmdbImageSet?> getImages(
|
||||
TmdbRequestConfig config,
|
||||
String tmdbId,
|
||||
String mediaType, {
|
||||
required String language,
|
||||
});
|
||||
|
||||
Future<TmdbEnrichedDetail?> getEnrichedDetail(
|
||||
TmdbRequestConfig config,
|
||||
String tmdbId,
|
||||
String mediaType, {
|
||||
required String language,
|
||||
List<String> appendToResponse,
|
||||
});
|
||||
|
||||
Future<TmdbVideosRes?> getVideos(
|
||||
TmdbRequestConfig config,
|
||||
String tmdbId,
|
||||
String mediaType, {
|
||||
required String language,
|
||||
});
|
||||
|
||||
Future<TmdbPersonDetail?> getPersonDetail(
|
||||
TmdbRequestConfig config,
|
||||
int personId, {
|
||||
required String language,
|
||||
});
|
||||
|
||||
Future<TmdbPersonCreditsRes?> getPersonCredits(
|
||||
TmdbRequestConfig config,
|
||||
int personId, {
|
||||
required String language,
|
||||
});
|
||||
|
||||
Future<TmdbMediaDetail?> getMediaDetail(
|
||||
TmdbRequestConfig config,
|
||||
String tmdbId,
|
||||
String mediaType, {
|
||||
required String language,
|
||||
});
|
||||
|
||||
Future<TmdbRecommendationsRes?> getTrending(
|
||||
TmdbRequestConfig config, {
|
||||
required String language,
|
||||
String mediaType,
|
||||
String timeWindow,
|
||||
});
|
||||
|
||||
Future<TmdbRecommendationsRes?> getPopularMovies(
|
||||
TmdbRequestConfig config, {
|
||||
required String language,
|
||||
});
|
||||
|
||||
Future<TmdbRecommendationsRes?> getPopularTv(
|
||||
TmdbRequestConfig config, {
|
||||
required String language,
|
||||
});
|
||||
|
||||
Future<TmdbRecommendationsRes?> getAiringTodayTv(
|
||||
TmdbRequestConfig config, {
|
||||
required String language,
|
||||
});
|
||||
|
||||
Future<TmdbRecommendationsRes?> getTopRatedMovies(
|
||||
TmdbRequestConfig config, {
|
||||
required String language,
|
||||
});
|
||||
|
||||
|
||||
Future<TmdbRecommendationsRes?> getDiscover(
|
||||
TmdbRequestConfig config,
|
||||
String mediaType, {
|
||||
required String language,
|
||||
String sortBy = 'popularity.desc',
|
||||
String? withGenres,
|
||||
String? voteCountGte,
|
||||
});
|
||||
|
||||
Future<bool> testConnection(
|
||||
TmdbRequestConfig config, {
|
||||
required String language,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import '../../contracts/trakt/trakt_token.dart';
|
||||
import '../../contracts/trakt/trakt_user.dart';
|
||||
|
||||
|
||||
abstract class TraktGateway {
|
||||
|
||||
Future<TraktToken> exchangeCode(String code);
|
||||
|
||||
|
||||
Future<TraktToken> refreshToken(String refreshToken);
|
||||
|
||||
|
||||
Future<TraktUser> getMe(String accessToken);
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
import '../../contracts/contracts.dart';
|
||||
import '../errors.dart';
|
||||
import '../ports/emby_gateway.dart';
|
||||
import '../ports/server_repository.dart';
|
||||
import '../ports/session_repository.dart';
|
||||
|
||||
class AuthByNameUseCase {
|
||||
final ServerRepository serverRepository;
|
||||
final SessionRepository sessionRepository;
|
||||
final EmbyGateway embyGateway;
|
||||
final DateTime Function() now;
|
||||
|
||||
AuthByNameUseCase({
|
||||
required this.serverRepository,
|
||||
required this.sessionRepository,
|
||||
required this.embyGateway,
|
||||
DateTime Function()? now,
|
||||
}) : now = now ?? DateTime.now;
|
||||
|
||||
Future<void> execute(AuthByNameReq input) async {
|
||||
final username = input.username.trim();
|
||||
final password = input.password;
|
||||
if (username.isEmpty || password.isEmpty) {
|
||||
throw DomainError(ErrorCode.invalidParams, '用户名或密码不能为空');
|
||||
}
|
||||
|
||||
final server = await serverRepository.findById(input.serverId);
|
||||
if (server == null) {
|
||||
throw DomainError(
|
||||
ErrorCode.storeNotFound,
|
||||
'服务器不存在',
|
||||
details: {'serverId': input.serverId},
|
||||
);
|
||||
}
|
||||
|
||||
final auth = await embyGateway.authenticateByName(
|
||||
baseUrl: server.baseUrl,
|
||||
username: username,
|
||||
password: password,
|
||||
);
|
||||
|
||||
final nowIso = now().toUtc().toIso8601String();
|
||||
await sessionRepository.set(
|
||||
SessionData(
|
||||
serverId: server.id,
|
||||
token: auth.accessToken,
|
||||
userId: auth.userId,
|
||||
userName: auth.userName,
|
||||
password: password,
|
||||
createdAt: nowIso,
|
||||
),
|
||||
);
|
||||
await serverRepository.touchConnectedAt(server.id, nowIso);
|
||||
}
|
||||
}
|
||||
|
||||
class LogoutUseCase {
|
||||
final SessionRepository sessionRepository;
|
||||
LogoutUseCase({required this.sessionRepository});
|
||||
|
||||
Future<void> execute([String? serverId]) => sessionRepository.clear(serverId);
|
||||
}
|
||||
|
||||
class ListSessionsUseCase {
|
||||
final SessionRepository sessionRepository;
|
||||
final ServerRepository serverRepository;
|
||||
|
||||
ListSessionsUseCase({
|
||||
required this.sessionRepository,
|
||||
required this.serverRepository,
|
||||
});
|
||||
|
||||
Future<SessionListRes> execute() async {
|
||||
final sessions = await sessionRepository.listAll();
|
||||
final activeServerId = await sessionRepository.getActiveServerId();
|
||||
final result = <AuthedSession>[];
|
||||
|
||||
for (final session in sessions) {
|
||||
final server = await serverRepository.findById(session.serverId);
|
||||
if (server != null) {
|
||||
result.add(
|
||||
AuthedSession(
|
||||
serverId: session.serverId,
|
||||
serverName: server.name,
|
||||
serverUrl: server.baseUrl,
|
||||
token: session.token,
|
||||
userId: session.userId,
|
||||
userName: session.userName,
|
||||
password: session.password,
|
||||
isActive: session.serverId == activeServerId,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return SessionListRes(sessions: result, activeServerId: activeServerId);
|
||||
}
|
||||
}
|
||||
|
||||
class SwitchSessionUseCase {
|
||||
final SessionRepository sessionRepository;
|
||||
|
||||
SwitchSessionUseCase({required this.sessionRepository});
|
||||
|
||||
Future<void> execute(String serverId) async {
|
||||
final session = await sessionRepository.getByServerId(serverId);
|
||||
if (session == null) {
|
||||
throw DomainError(
|
||||
ErrorCode.storeNotFound,
|
||||
'该服务器没有可用会话',
|
||||
details: {'serverId': serverId},
|
||||
);
|
||||
}
|
||||
await sessionRepository.setActive(serverId);
|
||||
}
|
||||
}
|
||||
|
||||
class ChangePasswordUseCase {
|
||||
final SessionRepository sessionRepository;
|
||||
final ServerRepository serverRepository;
|
||||
final EmbyGateway embyGateway;
|
||||
|
||||
ChangePasswordUseCase({
|
||||
required this.sessionRepository,
|
||||
required this.serverRepository,
|
||||
required this.embyGateway,
|
||||
});
|
||||
|
||||
Future<void> execute({
|
||||
required String serverId,
|
||||
required String currentPassword,
|
||||
required String newPassword,
|
||||
}) async {
|
||||
if (currentPassword.isEmpty || newPassword.isEmpty) {
|
||||
throw DomainError(ErrorCode.invalidParams, '密码不能为空');
|
||||
}
|
||||
|
||||
final session = await sessionRepository.getByServerId(serverId);
|
||||
if (session == null) {
|
||||
throw DomainError(ErrorCode.storeNotFound, '该服务器没有可用会话');
|
||||
}
|
||||
final server = await serverRepository.findById(serverId);
|
||||
if (server == null) {
|
||||
throw DomainError(ErrorCode.storeNotFound, '服务器不存在');
|
||||
}
|
||||
|
||||
final ctx = AuthedRequestContext(
|
||||
baseUrl: server.baseUrl,
|
||||
token: session.token,
|
||||
userId: session.userId,
|
||||
);
|
||||
|
||||
await embyGateway.changePassword(
|
||||
ctx: ctx,
|
||||
currentPassword: currentPassword,
|
||||
newPassword: newPassword,
|
||||
);
|
||||
|
||||
await sessionRepository.setMany([session.copyWith(password: newPassword)]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import '../../contracts/contracts.dart';
|
||||
import '../errors.dart';
|
||||
import '../ports/danmaku_sources_store.dart';
|
||||
import '../ports/script_widget_repository.dart';
|
||||
import '../ports/server_repository.dart';
|
||||
import '../ports/session_repository.dart';
|
||||
|
||||
const int kBackupSchemaVersion = 1;
|
||||
|
||||
class BackupExportResult {
|
||||
final String json;
|
||||
final int serverCount;
|
||||
final int sessionCount;
|
||||
final int danmakuSourceCount;
|
||||
final int scriptWidgetCount;
|
||||
final int scriptWidgetSubscriptionCount;
|
||||
|
||||
const BackupExportResult({
|
||||
required this.json,
|
||||
required this.serverCount,
|
||||
required this.sessionCount,
|
||||
required this.danmakuSourceCount,
|
||||
required this.scriptWidgetCount,
|
||||
required this.scriptWidgetSubscriptionCount,
|
||||
});
|
||||
}
|
||||
|
||||
class BackupImportResult {
|
||||
final int serverCount;
|
||||
final int sessionCount;
|
||||
final int danmakuSourceCount;
|
||||
final int scriptWidgetCount;
|
||||
final int scriptWidgetSubscriptionCount;
|
||||
|
||||
const BackupImportResult({
|
||||
required this.serverCount,
|
||||
required this.sessionCount,
|
||||
required this.danmakuSourceCount,
|
||||
required this.scriptWidgetCount,
|
||||
required this.scriptWidgetSubscriptionCount,
|
||||
});
|
||||
}
|
||||
|
||||
class ExportBackupUseCase {
|
||||
final ServerRepository serverRepository;
|
||||
final SessionRepository sessionRepository;
|
||||
final DanmakuSourcesStore danmakuSourcesStore;
|
||||
final ScriptWidgetRepository scriptWidgetRepository;
|
||||
final DateTime Function() now;
|
||||
|
||||
ExportBackupUseCase({
|
||||
required this.serverRepository,
|
||||
required this.sessionRepository,
|
||||
required this.danmakuSourcesStore,
|
||||
required this.scriptWidgetRepository,
|
||||
DateTime Function()? now,
|
||||
}) : now = (now ?? DateTime.now);
|
||||
|
||||
Future<BackupExportResult> execute() async {
|
||||
final servers = await serverRepository.list();
|
||||
final sessions = await sessionRepository.listAll();
|
||||
final danmakuSources = await danmakuSourcesStore.list();
|
||||
final scriptWidgets = await scriptWidgetRepository.listWidgets();
|
||||
final scriptWidgetSubscriptions =
|
||||
await scriptWidgetRepository.listSubscriptions();
|
||||
final bundle = BackupBundle(
|
||||
version: kBackupSchemaVersion,
|
||||
exportedAt: now().toUtc().toIso8601String(),
|
||||
servers: servers,
|
||||
sessions: sessions,
|
||||
danmakuSources: danmakuSources,
|
||||
scriptWidgets: scriptWidgets,
|
||||
scriptWidgetSubscriptions: scriptWidgetSubscriptions,
|
||||
);
|
||||
final json = const JsonEncoder.withIndent(' ').convert(bundle.toJson());
|
||||
return BackupExportResult(
|
||||
json: json,
|
||||
serverCount: servers.length,
|
||||
sessionCount: sessions.length,
|
||||
danmakuSourceCount: danmakuSources.length,
|
||||
scriptWidgetCount: scriptWidgets.length,
|
||||
scriptWidgetSubscriptionCount: scriptWidgetSubscriptions.length,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ImportBackupUseCase {
|
||||
final ServerRepository serverRepository;
|
||||
final SessionRepository sessionRepository;
|
||||
final DanmakuSourcesStore danmakuSourcesStore;
|
||||
final ScriptWidgetRepository scriptWidgetRepository;
|
||||
|
||||
ImportBackupUseCase({
|
||||
required this.serverRepository,
|
||||
required this.sessionRepository,
|
||||
required this.danmakuSourcesStore,
|
||||
required this.scriptWidgetRepository,
|
||||
});
|
||||
|
||||
Future<BackupImportResult> execute(String rawJson) async {
|
||||
final BackupBundle bundle;
|
||||
try {
|
||||
final decoded = jsonDecode(rawJson);
|
||||
if (decoded is! Map<String, dynamic>) {
|
||||
throw DomainError(ErrorCode.invalidParams, '备份文件格式不正确');
|
||||
}
|
||||
bundle = BackupBundle.fromJson(decoded);
|
||||
} on DomainError {
|
||||
rethrow;
|
||||
} catch (e) {
|
||||
throw DomainError(
|
||||
ErrorCode.invalidParams,
|
||||
'备份文件解析失败',
|
||||
details: {'reason': e.toString()},
|
||||
);
|
||||
}
|
||||
|
||||
if (bundle.version != kBackupSchemaVersion) {
|
||||
throw DomainError(
|
||||
ErrorCode.invalidParams,
|
||||
'不支持的备份文件版本:${bundle.version}',
|
||||
details: {'expected': kBackupSchemaVersion, 'got': bundle.version},
|
||||
);
|
||||
}
|
||||
|
||||
for (final server in bundle.servers) {
|
||||
await serverRepository.update(server);
|
||||
}
|
||||
await sessionRepository.setMany(bundle.sessions);
|
||||
|
||||
final mergedSources = mergeDanmakuSourcesByUrl(
|
||||
local: await danmakuSourcesStore.list(),
|
||||
remote: bundle.danmakuSources,
|
||||
);
|
||||
await danmakuSourcesStore.replaceAll(mergedSources);
|
||||
|
||||
for (final scriptWidget in bundle.scriptWidgets) {
|
||||
await scriptWidgetRepository.upsertWidget(scriptWidget);
|
||||
}
|
||||
for (final subscription in bundle.scriptWidgetSubscriptions) {
|
||||
await scriptWidgetRepository.upsertSubscription(subscription);
|
||||
}
|
||||
|
||||
return BackupImportResult(
|
||||
serverCount: bundle.servers.length,
|
||||
sessionCount: bundle.sessions.length,
|
||||
danmakuSourceCount: mergedSources.length,
|
||||
scriptWidgetCount: bundle.scriptWidgets.length,
|
||||
scriptWidgetSubscriptionCount: bundle.scriptWidgetSubscriptions.length,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
List<DanmakuSource> mergeDanmakuSourcesByUrl({
|
||||
required List<DanmakuSource> local,
|
||||
required List<DanmakuSource> remote,
|
||||
}) {
|
||||
final localByUrl = <String, DanmakuSource>{
|
||||
for (final s in local) s.url.trim(): s,
|
||||
};
|
||||
final out = <DanmakuSource>[];
|
||||
final seen = <String>{};
|
||||
for (final r in remote) {
|
||||
final url = r.url.trim();
|
||||
if (url.isEmpty || seen.contains(url)) continue;
|
||||
seen.add(url);
|
||||
final hit = localByUrl[url];
|
||||
if (hit != null) {
|
||||
out.add(hit.copyWith(name: hit.name.trim().isEmpty ? r.name : hit.name));
|
||||
} else {
|
||||
out.add(
|
||||
DanmakuSource(
|
||||
id: r.id.trim().isEmpty
|
||||
? 'src_${url.hashCode.toUnsigned(32).toRadixString(16)}'
|
||||
: r.id,
|
||||
name: r.name,
|
||||
url: url,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
import 'dart:async';
|
||||
|
||||
import '../../../shared/utils/app_logger.dart';
|
||||
import '../../contracts/contracts.dart';
|
||||
import '../ports/emby_gateway.dart';
|
||||
import '../ports/server_repository.dart';
|
||||
import '../ports/session_repository.dart';
|
||||
import 'usecase_helpers.dart';
|
||||
|
||||
const _ucTag = 'UseCase';
|
||||
|
||||
|
||||
Stream<GlobalSearchServerResult> _fanOutPerServer({
|
||||
required SessionRepository sessionRepository,
|
||||
required ServerRepository serverRepository,
|
||||
required Future<List<EmbyRawItem>?> Function(AuthedRequestContext context)
|
||||
fetchItems,
|
||||
required String logTag,
|
||||
}) {
|
||||
final controller = StreamController<GlobalSearchServerResult>();
|
||||
() async {
|
||||
try {
|
||||
final allSessions = await sessionRepository.listAll();
|
||||
final allServers = await serverRepository.list();
|
||||
final serverMap = {for (final s in allServers) s.id: s};
|
||||
|
||||
final futures = allSessions.map((session) async {
|
||||
final server = serverMap[session.serverId];
|
||||
if (server == null) return;
|
||||
final context = AuthedRequestContext(
|
||||
baseUrl: server.baseUrl,
|
||||
token: session.token,
|
||||
userId: session.userId,
|
||||
);
|
||||
try {
|
||||
final items = await fetchItems(context);
|
||||
if (items == null || items.isEmpty) return;
|
||||
controller.add(
|
||||
GlobalSearchServerResult(
|
||||
serverId: server.id,
|
||||
serverName: server.name,
|
||||
serverUrl: server.baseUrl,
|
||||
token: session.token,
|
||||
userId: session.userId,
|
||||
items: items,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
AppLogger.warn(_ucTag, '$logTag on ${server.name} failed', e);
|
||||
}
|
||||
});
|
||||
|
||||
await Future.wait(futures);
|
||||
} catch (e) {
|
||||
controller.addError(e);
|
||||
} finally {
|
||||
controller.close();
|
||||
}
|
||||
}();
|
||||
return controller.stream;
|
||||
}
|
||||
|
||||
class GetLibraryViewsUseCase extends AuthedUseCase {
|
||||
GetLibraryViewsUseCase({
|
||||
required super.sessionRepository,
|
||||
required super.serverRepository,
|
||||
required super.embyGateway,
|
||||
});
|
||||
|
||||
Future<LibraryViewsRes> execute() async {
|
||||
return embyGateway.getUserViews(await ctx());
|
||||
}
|
||||
}
|
||||
|
||||
class GetLibraryResumeUseCase extends AuthedUseCase {
|
||||
GetLibraryResumeUseCase({
|
||||
required super.sessionRepository,
|
||||
required super.serverRepository,
|
||||
required super.embyGateway,
|
||||
});
|
||||
|
||||
Future<LibraryResumeRes> execute() async {
|
||||
return embyGateway.getResumeItems(await ctx(), limit: 20);
|
||||
}
|
||||
}
|
||||
|
||||
class GetLibraryLatestUseCase extends AuthedUseCase {
|
||||
GetLibraryLatestUseCase({
|
||||
required super.sessionRepository,
|
||||
required super.serverRepository,
|
||||
required super.embyGateway,
|
||||
});
|
||||
|
||||
Future<LibraryLatestRes> execute(
|
||||
LibraryLatestReq input, {
|
||||
CancellationToken? cancelToken,
|
||||
}) async {
|
||||
return embyGateway.getLatestItems(
|
||||
await ctx(),
|
||||
input.parentId,
|
||||
limit: input.limit ?? 16,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GetLibraryItemsUseCase extends AuthedUseCase {
|
||||
GetLibraryItemsUseCase({
|
||||
required super.sessionRepository,
|
||||
required super.serverRepository,
|
||||
required super.embyGateway,
|
||||
});
|
||||
|
||||
Future<LibraryItemsRes> execute(LibraryItemsReq input) async {
|
||||
final items = await embyGateway.getLibraryItems(
|
||||
ctx: await ctx(),
|
||||
parentId: input.parentId,
|
||||
includeItemTypes: input.includeItemTypes,
|
||||
genreIds: input.genreIds,
|
||||
searchTerm: input.searchTerm,
|
||||
anyProviderIdEquals: input.anyProviderIdEquals,
|
||||
fields: input.fields,
|
||||
enableImageTypes: input.enableImageTypes,
|
||||
groupProgramsBySeries: input.groupProgramsBySeries,
|
||||
imageTypeLimit: input.imageTypeLimit,
|
||||
startIndex: input.startIndex,
|
||||
limit: input.limit,
|
||||
recursive: input.recursive,
|
||||
sortBy: input.sortBy,
|
||||
sortOrder: input.sortOrder,
|
||||
);
|
||||
return LibraryItemsRes(
|
||||
parentId: input.parentId ?? '__global__',
|
||||
items: items,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class GetHomeBannerUseCase extends AuthedUseCase {
|
||||
GetHomeBannerUseCase({
|
||||
required super.sessionRepository,
|
||||
required super.serverRepository,
|
||||
required super.embyGateway,
|
||||
});
|
||||
|
||||
Future<List<EmbyRawItem>> execute({int fetchLimit = 24, int take = 8}) async {
|
||||
final items = await embyGateway.getLibraryItems(
|
||||
ctx: await ctx(),
|
||||
includeItemTypes: 'Movie,Series',
|
||||
recursive: true,
|
||||
sortBy: 'DateCreated',
|
||||
sortOrder: SortOrder.descending,
|
||||
fields:
|
||||
'BasicSyncInfo,CommunityRating,ProductionYear,Overview,Genres,ProviderIds,UserData',
|
||||
enableImageTypes: 'Backdrop,Logo,Primary,Thumb',
|
||||
imageTypeLimit: 2,
|
||||
limit: fetchLimit,
|
||||
);
|
||||
return items
|
||||
.where((it) => (it.BackdropImageTags ?? const []).isNotEmpty)
|
||||
.take(take)
|
||||
.toList(growable: false);
|
||||
}
|
||||
}
|
||||
|
||||
class GlobalKeywordSearchUseCase extends AuthedUseCase {
|
||||
GlobalKeywordSearchUseCase({
|
||||
required super.sessionRepository,
|
||||
required super.serverRepository,
|
||||
required super.embyGateway,
|
||||
});
|
||||
|
||||
static const _defaultTypes = 'Movie,Series';
|
||||
|
||||
Stream<GlobalSearchServerResult> executeStream(GlobalSearchReq input) {
|
||||
final controller = StreamController<GlobalSearchServerResult>();
|
||||
() async {
|
||||
final keyword = input.keyword.trim();
|
||||
if (keyword.isEmpty) {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var sessions = await sessionRepository.listAll();
|
||||
if (input.serverId != null) {
|
||||
sessions = sessions
|
||||
.where((s) => s.serverId == input.serverId)
|
||||
.toList();
|
||||
}
|
||||
final allServers = await serverRepository.list();
|
||||
final serverMap = {for (final s in allServers) s.id: s};
|
||||
final types = input.includeItemTypes ?? _defaultTypes;
|
||||
|
||||
final futures = sessions.map((session) async {
|
||||
final result = await _searchOne(
|
||||
session: session,
|
||||
server: serverMap[session.serverId],
|
||||
keyword: keyword,
|
||||
types: types,
|
||||
limit: input.limitPerServer ?? 30,
|
||||
);
|
||||
if (result != null) controller.add(result);
|
||||
});
|
||||
|
||||
await Future.wait(futures);
|
||||
} catch (error, stackTrace) {
|
||||
controller.addError(error, stackTrace);
|
||||
} finally {
|
||||
controller.close();
|
||||
}
|
||||
}();
|
||||
return controller.stream;
|
||||
}
|
||||
|
||||
Future<GlobalSearchServerResult?> _searchOne({
|
||||
required SessionData session,
|
||||
required EmbyServer? server,
|
||||
required String keyword,
|
||||
required String types,
|
||||
required int limit,
|
||||
}) async {
|
||||
if (server == null) return null;
|
||||
final c = AuthedRequestContext(
|
||||
baseUrl: server.baseUrl,
|
||||
token: session.token,
|
||||
userId: session.userId,
|
||||
);
|
||||
try {
|
||||
final items = await embyGateway.getLibraryItems(
|
||||
ctx: c,
|
||||
searchTerm: keyword,
|
||||
recursive: true,
|
||||
includeItemTypes: types,
|
||||
limit: limit,
|
||||
sortBy: 'SortName',
|
||||
sortOrder: SortOrder.ascending,
|
||||
);
|
||||
if (items.isEmpty) return null;
|
||||
return GlobalSearchServerResult(
|
||||
serverId: server.id,
|
||||
serverName: server.name,
|
||||
serverUrl: server.baseUrl,
|
||||
token: session.token,
|
||||
userId: session.userId,
|
||||
items: items,
|
||||
);
|
||||
} catch (e) {
|
||||
AppLogger.warn(_ucTag, 'GlobalKeywordSearch on ${server.name} failed', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GetAggregatedResumeUseCase extends AuthedUseCase {
|
||||
GetAggregatedResumeUseCase({
|
||||
required super.sessionRepository,
|
||||
required super.serverRepository,
|
||||
required super.embyGateway,
|
||||
});
|
||||
|
||||
Stream<GlobalSearchServerResult> executeStream({int? limitPerServer}) {
|
||||
return _fanOutPerServer(
|
||||
sessionRepository: sessionRepository,
|
||||
serverRepository: serverRepository,
|
||||
logTag: 'AggregatedResume',
|
||||
fetchItems: (context) async {
|
||||
final res = await embyGateway.getResumeItems(
|
||||
context,
|
||||
limit: limitPerServer ?? 20,
|
||||
);
|
||||
return res.Items;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GetAggregatedFavoritesUseCase extends AuthedUseCase {
|
||||
static const _defaultTypes = 'Movie,Series,Episode';
|
||||
|
||||
GetAggregatedFavoritesUseCase({
|
||||
required super.sessionRepository,
|
||||
required super.serverRepository,
|
||||
required super.embyGateway,
|
||||
});
|
||||
|
||||
Stream<GlobalSearchServerResult> executeStream({
|
||||
String includeItemTypes = _defaultTypes,
|
||||
int? limitPerServer,
|
||||
}) {
|
||||
final types = includeItemTypes.trim().isEmpty
|
||||
? _defaultTypes
|
||||
: includeItemTypes;
|
||||
return _fanOutPerServer(
|
||||
sessionRepository: sessionRepository,
|
||||
serverRepository: serverRepository,
|
||||
logTag: 'AggregatedFavorites',
|
||||
fetchItems: (context) async {
|
||||
final items = await embyGateway.getFavoriteItems(
|
||||
ctx: context,
|
||||
includeItemTypes: types,
|
||||
sortBy: 'SortName',
|
||||
sortOrder: SortOrder.ascending,
|
||||
);
|
||||
return limitPerServer == null
|
||||
? items
|
||||
: items.take(limitPerServer).toList(growable: false);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GetAggregatedRecentUseCase extends AuthedUseCase {
|
||||
static const _defaultTypes = 'Movie,Series';
|
||||
|
||||
GetAggregatedRecentUseCase({
|
||||
required super.sessionRepository,
|
||||
required super.serverRepository,
|
||||
required super.embyGateway,
|
||||
});
|
||||
|
||||
Stream<GlobalSearchServerResult> executeStream({
|
||||
String includeItemTypes = _defaultTypes,
|
||||
int? limitPerServer,
|
||||
}) {
|
||||
return _fanOutPerServer(
|
||||
sessionRepository: sessionRepository,
|
||||
serverRepository: serverRepository,
|
||||
logTag: 'AggregatedRecent',
|
||||
fetchItems: (context) async {
|
||||
return embyGateway.getLibraryItems(
|
||||
ctx: context,
|
||||
includeItemTypes: includeItemTypes,
|
||||
fields:
|
||||
'BasicSyncInfo,CollectionType,PrimaryImageAspectRatio,UserData,'
|
||||
'CommunityRating,ProviderIds,ProductionYear,ChildCount,Container,'
|
||||
'CanDelete,DateCreated',
|
||||
sortBy: 'DateCreated',
|
||||
sortOrder: SortOrder.descending,
|
||||
recursive: true,
|
||||
limit: limitPerServer ?? 20,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,913 @@
|
||||
import 'dart:async';
|
||||
|
||||
import '../../../shared/utils/app_logger.dart';
|
||||
import '../../contracts/contracts.dart';
|
||||
import '../ports/emby_gateway.dart';
|
||||
import 'usecase_helpers.dart';
|
||||
|
||||
const _ucTag = 'UseCase';
|
||||
|
||||
class GetMediaDetailUseCase extends AuthedUseCase {
|
||||
GetMediaDetailUseCase({
|
||||
required super.sessionRepository,
|
||||
required super.serverRepository,
|
||||
required super.embyGateway,
|
||||
});
|
||||
|
||||
Future<MediaDetailRes> execute(MediaDetailReq input) async {
|
||||
final base = await embyGateway.getItemDetail(await ctx(), input.itemId);
|
||||
return MediaDetailRes(base: base);
|
||||
}
|
||||
|
||||
|
||||
Future<MediaDetailRes> executeForServer({
|
||||
required String serverId,
|
||||
required String itemId,
|
||||
}) async {
|
||||
final ctx = await requireCtxForServer(serverId);
|
||||
final base = await embyGateway.getItemDetail(ctx, itemId);
|
||||
return MediaDetailRes(base: base);
|
||||
}
|
||||
}
|
||||
|
||||
class SetFavoriteUseCase extends AuthedUseCase {
|
||||
SetFavoriteUseCase({
|
||||
required super.sessionRepository,
|
||||
required super.serverRepository,
|
||||
required super.embyGateway,
|
||||
});
|
||||
|
||||
Future<FavoriteSetRes> execute(FavoriteSetReq input) async {
|
||||
final result = await embyGateway.setFavoriteStatus(
|
||||
ctx: await ctx(),
|
||||
itemId: input.itemId,
|
||||
isFavorite: input.isFavorite,
|
||||
);
|
||||
return FavoriteSetRes(itemId: input.itemId, isFavorite: result);
|
||||
}
|
||||
|
||||
|
||||
Future<FavoriteSetRes> executeForServer({
|
||||
required String serverId,
|
||||
required FavoriteSetReq input,
|
||||
}) async {
|
||||
final ctx = await requireCtxForServer(serverId);
|
||||
final result = await embyGateway.setFavoriteStatus(
|
||||
ctx: ctx,
|
||||
itemId: input.itemId,
|
||||
isFavorite: input.isFavorite,
|
||||
);
|
||||
return FavoriteSetRes(itemId: input.itemId, isFavorite: result);
|
||||
}
|
||||
}
|
||||
|
||||
class SetPlayedUseCase extends AuthedUseCase {
|
||||
SetPlayedUseCase({
|
||||
required super.sessionRepository,
|
||||
required super.serverRepository,
|
||||
required super.embyGateway,
|
||||
});
|
||||
|
||||
Future<PlayedSetRes> execute(PlayedSetReq input) async {
|
||||
final result = await embyGateway.setPlayedStatus(
|
||||
ctx: await ctx(),
|
||||
itemId: input.itemId,
|
||||
played: input.played,
|
||||
);
|
||||
return PlayedSetRes(itemId: input.itemId, played: result);
|
||||
}
|
||||
|
||||
|
||||
Future<PlayedSetRes> executeForServer({
|
||||
required String serverId,
|
||||
required PlayedSetReq input,
|
||||
}) async {
|
||||
final ctx = await requireCtxForServer(serverId);
|
||||
final result = await embyGateway.setPlayedStatus(
|
||||
ctx: ctx,
|
||||
itemId: input.itemId,
|
||||
played: input.played,
|
||||
);
|
||||
return PlayedSetRes(itemId: input.itemId, played: result);
|
||||
}
|
||||
}
|
||||
|
||||
class HideFromResumeUseCase extends AuthedUseCase {
|
||||
HideFromResumeUseCase({
|
||||
required super.sessionRepository,
|
||||
required super.serverRepository,
|
||||
required super.embyGateway,
|
||||
});
|
||||
|
||||
Future<HideFromResumeRes> execute(HideFromResumeReq input) async {
|
||||
final result = await embyGateway.hideFromResume(
|
||||
ctx: await ctx(),
|
||||
itemId: input.itemId,
|
||||
hide: input.hide,
|
||||
);
|
||||
return HideFromResumeRes(itemId: input.itemId, hide: result);
|
||||
}
|
||||
}
|
||||
|
||||
class GetSeriesEpisodesUseCase extends AuthedUseCase {
|
||||
GetSeriesEpisodesUseCase({
|
||||
required super.sessionRepository,
|
||||
required super.serverRepository,
|
||||
required super.embyGateway,
|
||||
});
|
||||
|
||||
Future<MediaEpisodesRes> execute(MediaEpisodesReq input) async {
|
||||
final items = await embyGateway.getSeriesEpisodes(
|
||||
await ctx(),
|
||||
input.seriesId,
|
||||
input.seasonId,
|
||||
);
|
||||
return MediaEpisodesRes(
|
||||
seriesId: input.seriesId,
|
||||
seasonId: input.seasonId,
|
||||
items: items,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Future<MediaEpisodesRes> executeForServer({
|
||||
required String serverId,
|
||||
required MediaEpisodesReq input,
|
||||
}) async {
|
||||
final ctx = await requireCtxForServer(serverId);
|
||||
final items = await embyGateway.getSeriesEpisodes(
|
||||
ctx,
|
||||
input.seriesId,
|
||||
input.seasonId,
|
||||
);
|
||||
return MediaEpisodesRes(
|
||||
seriesId: input.seriesId,
|
||||
seasonId: input.seasonId,
|
||||
items: items,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GetSeriesSeasonsUseCase extends AuthedUseCase {
|
||||
GetSeriesSeasonsUseCase({
|
||||
required super.sessionRepository,
|
||||
required super.serverRepository,
|
||||
required super.embyGateway,
|
||||
});
|
||||
|
||||
Future<SeriesSeasonsRes> execute(SeriesSeasonsReq input) async {
|
||||
final seasons = await embyGateway.getSeriesSeasons(
|
||||
await ctx(),
|
||||
input.seriesId,
|
||||
);
|
||||
return SeriesSeasonsRes(seriesId: input.seriesId, items: seasons);
|
||||
}
|
||||
|
||||
|
||||
Future<SeriesSeasonsRes> executeForServer({
|
||||
required String serverId,
|
||||
required SeriesSeasonsReq input,
|
||||
}) async {
|
||||
final ctx = await requireCtxForServer(serverId);
|
||||
final seasons = await embyGateway.getSeriesSeasons(ctx, input.seriesId);
|
||||
return SeriesSeasonsRes(seriesId: input.seriesId, items: seasons);
|
||||
}
|
||||
}
|
||||
|
||||
class GetSimilarItemsUseCase extends AuthedUseCase {
|
||||
GetSimilarItemsUseCase({
|
||||
required super.sessionRepository,
|
||||
required super.serverRepository,
|
||||
required super.embyGateway,
|
||||
});
|
||||
|
||||
Future<SimilarItemsRes> execute(SimilarItemsReq input) async {
|
||||
final items = await embyGateway.getSimilarItems(
|
||||
await ctx(),
|
||||
input.itemId,
|
||||
limit: input.limit,
|
||||
);
|
||||
return SimilarItemsRes(itemId: input.itemId, items: items);
|
||||
}
|
||||
}
|
||||
|
||||
class GetAdditionalPartsUseCase extends AuthedUseCase {
|
||||
GetAdditionalPartsUseCase({
|
||||
required super.sessionRepository,
|
||||
required super.serverRepository,
|
||||
required super.embyGateway,
|
||||
});
|
||||
|
||||
Future<AdditionalPartsRes> execute(AdditionalPartsReq input) async {
|
||||
final items = await embyGateway.getAdditionalParts(
|
||||
await ctx(),
|
||||
input.itemId,
|
||||
);
|
||||
return AdditionalPartsRes(itemId: input.itemId, items: items);
|
||||
}
|
||||
|
||||
|
||||
Future<AdditionalPartsRes> executeForServer({
|
||||
required String serverId,
|
||||
required String itemId,
|
||||
}) async {
|
||||
final ctx = await resolveAuthedContextForServer(
|
||||
sessionRepository: sessionRepository,
|
||||
serverRepository: serverRepository,
|
||||
serverId: serverId,
|
||||
);
|
||||
if (ctx == null) {
|
||||
return AdditionalPartsRes(itemId: itemId, items: const []);
|
||||
}
|
||||
final items = await embyGateway.getAdditionalParts(ctx, itemId);
|
||||
return AdditionalPartsRes(itemId: itemId, items: items);
|
||||
}
|
||||
}
|
||||
|
||||
class GetSpecialFeaturesUseCase extends AuthedUseCase {
|
||||
GetSpecialFeaturesUseCase({
|
||||
required super.sessionRepository,
|
||||
required super.serverRepository,
|
||||
required super.embyGateway,
|
||||
});
|
||||
|
||||
Future<List<EmbyRawItem>> execute(String itemId) async {
|
||||
return embyGateway.getSpecialFeatures(await ctx(), itemId);
|
||||
}
|
||||
}
|
||||
|
||||
class CrossServerSearchUseCase extends AuthedUseCase {
|
||||
CrossServerSearchUseCase({
|
||||
required super.sessionRepository,
|
||||
required super.serverRepository,
|
||||
required super.embyGateway,
|
||||
});
|
||||
|
||||
|
||||
final Map<String, _SeriesIdentity?> _seriesIdentityCache = {};
|
||||
final Map<String, _EmptyServerSearchStreak> _emptyServerSearchStreaks = {};
|
||||
|
||||
static const _emptyServerSkipThreshold = 3;
|
||||
static const _emptyServerSkipTtl = Duration(hours: 1);
|
||||
|
||||
String _identityCacheKey(String serverId, String seriesPidQuery) =>
|
||||
'$serverId|$seriesPidQuery';
|
||||
|
||||
|
||||
Stream<List<CrossServerSourceCard>> executeStream(
|
||||
CrossServerSearchReq input, {
|
||||
CancellationToken? cancelToken,
|
||||
}) {
|
||||
final controller = StreamController<List<CrossServerSourceCard>>();
|
||||
final sw = Stopwatch()..start();
|
||||
() async {
|
||||
try {
|
||||
final activeServerId = await sessionRepository.getActiveServerId();
|
||||
final excludedServerId = input.excludedServerId ?? activeServerId;
|
||||
final allSessions = await sessionRepository.listAll();
|
||||
final allServers = await serverRepository.list();
|
||||
final serverMap = {for (final s in allServers) s.id: s};
|
||||
|
||||
final acc = <CrossServerSourceCard>[];
|
||||
var probed = 0;
|
||||
var skipped = 0;
|
||||
final futures = <Future<void>>[];
|
||||
for (final session in allSessions) {
|
||||
if (session.serverId == excludedServerId) continue;
|
||||
final server = serverMap[session.serverId];
|
||||
if (server == null) continue;
|
||||
if (_shouldSkipEmptyServer(server)) {
|
||||
skipped++;
|
||||
final streak = _emptyServerSearchStreaks[server.id];
|
||||
AppLogger.debug(
|
||||
'DetailTiming',
|
||||
'xserver.server name=${server.name} skipped emptyStreak=${streak?.count ?? 0}',
|
||||
);
|
||||
continue;
|
||||
}
|
||||
probed++;
|
||||
futures.add(() async {
|
||||
final serverSw = Stopwatch()..start();
|
||||
final cards = await _buildCardsForServer(
|
||||
server,
|
||||
session,
|
||||
input,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
AppLogger.debug(
|
||||
'DetailTiming',
|
||||
'xserver.server name=${server.name} cards=${cards.length} ms=${serverSw.elapsedMilliseconds}',
|
||||
);
|
||||
if (cancelToken?.isCancelled == true) return;
|
||||
_recordServerSearchResult(server, cards);
|
||||
if (cards.isEmpty) return;
|
||||
acc.addAll(cards);
|
||||
if (!controller.isClosed) {
|
||||
controller.add(List.unmodifiable(acc));
|
||||
}
|
||||
}());
|
||||
}
|
||||
await Future.wait(futures);
|
||||
AppLogger.debug(
|
||||
'DetailTiming',
|
||||
'xserver.total probed=$probed skipped=$skipped cards=${acc.length} ms=${sw.elapsedMilliseconds}',
|
||||
);
|
||||
} catch (e) {
|
||||
if (!controller.isClosed) controller.addError(e);
|
||||
} finally {
|
||||
if (!controller.isClosed) await controller.close();
|
||||
}
|
||||
}();
|
||||
return controller.stream;
|
||||
}
|
||||
|
||||
bool _shouldSkipEmptyServer(EmbyServer server) {
|
||||
final streak = _emptyServerSearchStreaks[server.id];
|
||||
final skipUntil = streak?.skipUntil;
|
||||
if (skipUntil == null) return false;
|
||||
if (DateTime.now().isBefore(skipUntil)) return true;
|
||||
_emptyServerSearchStreaks.remove(server.id);
|
||||
return false;
|
||||
}
|
||||
|
||||
void _recordServerSearchResult(
|
||||
EmbyServer server,
|
||||
List<CrossServerSourceCard> cards,
|
||||
) {
|
||||
if (cards.isNotEmpty) {
|
||||
_emptyServerSearchStreaks.remove(server.id);
|
||||
return;
|
||||
}
|
||||
final streak = _emptyServerSearchStreaks.putIfAbsent(
|
||||
server.id,
|
||||
_EmptyServerSearchStreak.new,
|
||||
);
|
||||
streak.count++;
|
||||
if (streak.count < _emptyServerSkipThreshold) return;
|
||||
streak.skipUntil = DateTime.now().add(_emptyServerSkipTtl);
|
||||
AppLogger.debug(
|
||||
'DetailTiming',
|
||||
'xserver.server name=${server.name} emptyStreak=${streak.count} '
|
||||
'skipUntil=${streak.skipUntil!.toIso8601String()}',
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<CrossServerSourceCard>> _buildCardsForServer(
|
||||
EmbyServer server,
|
||||
SessionData session,
|
||||
CrossServerSearchReq req, {
|
||||
CancellationToken? cancelToken,
|
||||
}) async {
|
||||
final ctx = AuthedRequestContext(
|
||||
baseUrl: server.baseUrl,
|
||||
token: session.token,
|
||||
userId: session.userId,
|
||||
);
|
||||
try {
|
||||
if (req.anchorType == 'Movie') {
|
||||
return await _buildMovieCards(
|
||||
ctx,
|
||||
server,
|
||||
session,
|
||||
req,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
}
|
||||
if (req.anchorType == 'Episode') {
|
||||
return await _buildEpisodeCards(
|
||||
ctx,
|
||||
server,
|
||||
session,
|
||||
req,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
}
|
||||
return const [];
|
||||
} on CancelledException {
|
||||
return const [];
|
||||
} catch (e) {
|
||||
AppLogger.warn(_ucTag, 'CrossServerSearch on ${server.name} failed', e);
|
||||
return const [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<CrossServerSourceCard>> _buildMovieCards(
|
||||
AuthedRequestContext ctx,
|
||||
EmbyServer server,
|
||||
SessionData session,
|
||||
CrossServerSearchReq req, {
|
||||
CancellationToken? cancelToken,
|
||||
}) async {
|
||||
const movieFields = 'ProviderIds,UserData,MediaSources,MediaStreams';
|
||||
final providerQuery = _buildProviderIdQuery(req.providerIds);
|
||||
var items = <EmbyRawItem>[];
|
||||
if (providerQuery != null && providerQuery.isNotEmpty) {
|
||||
final providerItems = await embyGateway.getLibraryItems(
|
||||
ctx: ctx,
|
||||
includeItemTypes: 'Movie',
|
||||
anyProviderIdEquals: providerQuery,
|
||||
fields: movieFields,
|
||||
limit: 10,
|
||||
recursive: true,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
items = providerItems
|
||||
.where((item) => _itemMatchesAnyProviderId(item, req.providerIds))
|
||||
.toList(growable: false);
|
||||
final droppedItemCount = providerItems.length - items.length;
|
||||
if (droppedItemCount > 0) {
|
||||
AppLogger.debug(
|
||||
'DetailTiming',
|
||||
'xserver.provider-filter server=${server.name} '
|
||||
'dropped=$droppedItemCount returned=${providerItems.length}',
|
||||
);
|
||||
}
|
||||
}
|
||||
if (items.isEmpty && req.itemName.isNotEmpty) {
|
||||
final nameSearchItems = await embyGateway.getLibraryItems(
|
||||
ctx: ctx,
|
||||
includeItemTypes: 'Movie',
|
||||
searchTerm: req.itemName,
|
||||
fields: movieFields,
|
||||
limit: 20,
|
||||
recursive: true,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
items = nameSearchItems
|
||||
.where(
|
||||
(item) => !_itemHasConflictingProviderId(item, req.providerIds),
|
||||
)
|
||||
.toList(growable: false);
|
||||
}
|
||||
final cards = <CrossServerSourceCard>[];
|
||||
if (cancelToken?.isCancelled == true) return List.unmodifiable(cards);
|
||||
final detailedList = await Future.wait(
|
||||
items.map(
|
||||
(it) => _fetchDetailedItem(ctx, server, it, cancelToken: cancelToken),
|
||||
),
|
||||
);
|
||||
if (cancelToken?.isCancelled == true) return List.unmodifiable(cards);
|
||||
for (final detailed in detailedList) {
|
||||
cards.addAll(_expandToCards(server, session, detailed));
|
||||
}
|
||||
return List.unmodifiable(cards);
|
||||
}
|
||||
|
||||
|
||||
Future<EmbyRawItem> _fetchDetailedItem(
|
||||
AuthedRequestContext ctx,
|
||||
EmbyServer server,
|
||||
EmbyRawItem item, {
|
||||
CancellationToken? cancelToken,
|
||||
}) async {
|
||||
if (cancelToken?.isCancelled == true) return item;
|
||||
try {
|
||||
final full = await embyGateway.getItemDetail(ctx, item.Id);
|
||||
final fullSources = full.extra['MediaSources'];
|
||||
if (fullSources is List && fullSources.isNotEmpty) return full;
|
||||
return item;
|
||||
} catch (e) {
|
||||
AppLogger.warn(
|
||||
_ucTag,
|
||||
'CrossServer detail fetch failed on ${server.name} item=${item.Id}',
|
||||
e,
|
||||
);
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<CrossServerSourceCard>> _buildEpisodeCards(
|
||||
AuthedRequestContext ctx,
|
||||
EmbyServer server,
|
||||
SessionData session,
|
||||
CrossServerSearchReq req, {
|
||||
CancellationToken? cancelToken,
|
||||
}) async {
|
||||
const episodeFields = 'ProviderIds,UserData,MediaSources,MediaStreams';
|
||||
|
||||
final providerQuery = _buildProviderIdQuery(req.providerIds);
|
||||
if (providerQuery != null && providerQuery.isNotEmpty) {
|
||||
final episodes = await embyGateway.getLibraryItems(
|
||||
ctx: ctx,
|
||||
includeItemTypes: 'Episode',
|
||||
anyProviderIdEquals: providerQuery,
|
||||
fields: episodeFields,
|
||||
limit: 5,
|
||||
recursive: true,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
final exact = _matchEpisodeBySE(
|
||||
episodes,
|
||||
req.parentIndexNumber,
|
||||
req.indexNumber,
|
||||
);
|
||||
if (exact != null) {
|
||||
final detailed = await _fetchDetailedItem(
|
||||
ctx,
|
||||
server,
|
||||
exact,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
return _expandToCards(server, session, detailed);
|
||||
}
|
||||
}
|
||||
|
||||
final seriesProviderIds = req.seriesProviderIds;
|
||||
if (seriesProviderIds == null || seriesProviderIds.isEmpty) {
|
||||
return const [];
|
||||
}
|
||||
|
||||
final identity = await _resolveSeriesIdentity(
|
||||
ctx,
|
||||
server.id,
|
||||
seriesProviderIds,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
if (identity == null) return const [];
|
||||
|
||||
EmbyRawSeason? matchedSeason;
|
||||
for (final s in identity.seasons) {
|
||||
if (s.IndexNumber == req.parentIndexNumber) {
|
||||
matchedSeason = s;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (matchedSeason == null) return const [];
|
||||
|
||||
final episodes = await embyGateway.getSeriesEpisodes(
|
||||
ctx,
|
||||
identity.remoteSeriesId,
|
||||
matchedSeason.Id,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
final target = _matchEpisodeBySE(
|
||||
episodes,
|
||||
req.parentIndexNumber,
|
||||
req.indexNumber,
|
||||
);
|
||||
if (target == null) return const [];
|
||||
final detailed = await _fetchDetailedItem(
|
||||
ctx,
|
||||
server,
|
||||
target,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
return _expandToCards(server, session, detailed);
|
||||
}
|
||||
|
||||
|
||||
Future<_SeriesIdentity?> _resolveSeriesIdentity(
|
||||
AuthedRequestContext ctx,
|
||||
String serverId,
|
||||
Map<String, String> seriesProviderIds, {
|
||||
CancellationToken? cancelToken,
|
||||
}) async {
|
||||
final seriesPidQuery = _buildProviderIdQuery(seriesProviderIds);
|
||||
if (seriesPidQuery == null || seriesPidQuery.isEmpty) return null;
|
||||
final key = _identityCacheKey(serverId, seriesPidQuery);
|
||||
if (_seriesIdentityCache.containsKey(key)) {
|
||||
return _seriesIdentityCache[key];
|
||||
}
|
||||
final seriesList = await embyGateway.getLibraryItems(
|
||||
ctx: ctx,
|
||||
includeItemTypes: 'Series',
|
||||
anyProviderIdEquals: seriesPidQuery,
|
||||
fields: 'ProviderIds',
|
||||
limit: 1,
|
||||
recursive: true,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
if (seriesList.isEmpty ||
|
||||
!_itemMatchesAnyProviderId(seriesList.first, seriesProviderIds)) {
|
||||
_seriesIdentityCache[key] = null;
|
||||
return null;
|
||||
}
|
||||
final remoteSeriesId = seriesList.first.Id;
|
||||
final seasons = await embyGateway.getSeriesSeasons(
|
||||
ctx,
|
||||
remoteSeriesId,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
final identity = _SeriesIdentity(
|
||||
remoteSeriesId: remoteSeriesId,
|
||||
seasons: seasons,
|
||||
);
|
||||
_seriesIdentityCache[key] = identity;
|
||||
return identity;
|
||||
}
|
||||
|
||||
|
||||
Future<void> prewarmSeriesIdentity(
|
||||
Map<String, String> seriesProviderIds, {
|
||||
CancellationToken? cancelToken,
|
||||
}) async {
|
||||
if (seriesProviderIds.isEmpty) return;
|
||||
|
||||
try {
|
||||
final activeServerId = await sessionRepository.getActiveServerId();
|
||||
final allSessions = await sessionRepository.listAll();
|
||||
final allServers = await serverRepository.list();
|
||||
final serverMap = {for (final s in allServers) s.id: s};
|
||||
|
||||
final futures = <Future<void>>[];
|
||||
for (final session in allSessions) {
|
||||
if (session.serverId == activeServerId) continue;
|
||||
final server = serverMap[session.serverId];
|
||||
if (server == null) continue;
|
||||
final ctx = AuthedRequestContext(
|
||||
baseUrl: server.baseUrl,
|
||||
token: session.token,
|
||||
userId: session.userId,
|
||||
);
|
||||
futures.add(() async {
|
||||
try {
|
||||
await _resolveSeriesIdentity(
|
||||
ctx,
|
||||
server.id,
|
||||
seriesProviderIds,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
} on CancelledException {
|
||||
return;
|
||||
} catch (e) {
|
||||
AppLogger.warn(
|
||||
_ucTag,
|
||||
'prewarmSeriesIdentity on ${server.name} failed',
|
||||
e,
|
||||
);
|
||||
}
|
||||
}());
|
||||
}
|
||||
await Future.wait(futures);
|
||||
} on CancelledException {
|
||||
|
||||
} catch (e) {
|
||||
AppLogger.warn(_ucTag, 'prewarmSeriesIdentity setup failed', e);
|
||||
}
|
||||
}
|
||||
|
||||
static List<CrossServerSourceCard> _expandToCards(
|
||||
EmbyServer server,
|
||||
SessionData session,
|
||||
EmbyRawItem item,
|
||||
) {
|
||||
final raw = item.extra['MediaSources'];
|
||||
if (raw is! List) return const [];
|
||||
final sources = raw
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(EmbyRawMediaSource.fromJson)
|
||||
.toList();
|
||||
final cards = <CrossServerSourceCard>[];
|
||||
for (final s in sources) {
|
||||
final id = s.Id;
|
||||
if (id == null) continue;
|
||||
cards.add(
|
||||
CrossServerSourceCard(
|
||||
serverId: server.id,
|
||||
serverName: server.name,
|
||||
serverUrl: server.baseUrl,
|
||||
token: session.token,
|
||||
userId: session.userId,
|
||||
landingItemId: item.Id,
|
||||
mediaSourceId: id,
|
||||
item: item,
|
||||
mediaSource: s,
|
||||
quality: _extractQualityFromSource(s),
|
||||
),
|
||||
);
|
||||
}
|
||||
return cards;
|
||||
}
|
||||
|
||||
static EmbyRawItem? _matchEpisodeBySE(
|
||||
List<EmbyRawItem> episodes,
|
||||
int? parentIndex,
|
||||
int? index,
|
||||
) {
|
||||
if (parentIndex == null || index == null) return null;
|
||||
for (final it in episodes) {
|
||||
final ps = (it.extra['ParentIndexNumber'] as num?)?.toInt();
|
||||
if (ps == parentIndex && it.IndexNumber == index) return it;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static bool _itemMatchesAnyProviderId(
|
||||
EmbyRawItem item,
|
||||
Map<String, String> requestedProviderIds,
|
||||
) {
|
||||
final normalizedRequestedProviderIds = normalizeProviderIds(
|
||||
requestedProviderIds,
|
||||
);
|
||||
if (normalizedRequestedProviderIds.isEmpty) return false;
|
||||
|
||||
final normalizedItemProviderIds = normalizedProviderIdsOfItem(item);
|
||||
for (final requestedEntry in normalizedRequestedProviderIds.entries) {
|
||||
if (normalizedItemProviderIds[requestedEntry.key] ==
|
||||
requestedEntry.value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool _itemHasConflictingProviderId(
|
||||
EmbyRawItem item,
|
||||
Map<String, String> requestedProviderIds,
|
||||
) {
|
||||
final normalizedRequestedProviderIds = normalizeProviderIds(
|
||||
requestedProviderIds,
|
||||
);
|
||||
if (normalizedRequestedProviderIds.isEmpty) return false;
|
||||
|
||||
final normalizedItemProviderIds = normalizedProviderIdsOfItem(item);
|
||||
for (final requestedEntry in normalizedRequestedProviderIds.entries) {
|
||||
final itemProviderId = normalizedItemProviderIds[requestedEntry.key];
|
||||
if (itemProviderId != null && itemProviderId != requestedEntry.value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static MediaQualityInfo? _extractQualityFromSource(EmbyRawMediaSource src) {
|
||||
final streams = src.MediaStreams ?? const [];
|
||||
if (streams.isEmpty || !streams.any((s) => s.Type == 'Video')) {
|
||||
final fallback = MediaQualityInfo.fromSourceName(
|
||||
src.Name,
|
||||
fallbackHeight: src.Height,
|
||||
container: src.Container,
|
||||
);
|
||||
AppLogger.debug(
|
||||
'DetailQuality',
|
||||
'name-fallback sourceId=${src.Id ?? 'null'} '
|
||||
'name="${src.Name ?? 'null'}" -> '
|
||||
'${fallback == null ? 'null' : '"${fallback.resolutionLabel} ${fallback.dynamicRangeLabel}"'}',
|
||||
);
|
||||
return fallback;
|
||||
}
|
||||
return MediaQualityInfo.fromMediaSource(src);
|
||||
}
|
||||
|
||||
static String? _buildProviderIdQuery(Map<String, String> providerIds) {
|
||||
if (providerIds.isEmpty) return null;
|
||||
final parts = <String>[];
|
||||
for (final entry in providerIds.entries) {
|
||||
if (entry.value.isNotEmpty) {
|
||||
parts.add('${entry.key}.${entry.value}');
|
||||
}
|
||||
}
|
||||
if (parts.isEmpty) return null;
|
||||
return parts.join(',');
|
||||
}
|
||||
}
|
||||
|
||||
class _SeriesIdentity {
|
||||
final String remoteSeriesId;
|
||||
final List<EmbyRawSeason> seasons;
|
||||
const _SeriesIdentity({required this.remoteSeriesId, required this.seasons});
|
||||
}
|
||||
|
||||
class _EmptyServerSearchStreak {
|
||||
int count = 0;
|
||||
DateTime? skipUntil;
|
||||
}
|
||||
|
||||
|
||||
String _buildAvailabilityProviderQuery({
|
||||
required String tmdbId,
|
||||
String? imdbId,
|
||||
}) {
|
||||
final parts = <String>['Tmdb.${tmdbId.trim()}'];
|
||||
final imdb = imdbId?.trim() ?? '';
|
||||
if (imdb.isNotEmpty) parts.add('Imdb.$imdb');
|
||||
return parts.join(',');
|
||||
}
|
||||
|
||||
|
||||
class TmdbAvailabilityUseCase extends AuthedUseCase {
|
||||
TmdbAvailabilityUseCase({
|
||||
required super.sessionRepository,
|
||||
required super.serverRepository,
|
||||
required super.embyGateway,
|
||||
this.settleTimeout = const Duration(seconds: 3),
|
||||
});
|
||||
|
||||
final Duration settleTimeout;
|
||||
|
||||
Stream<List<TmdbLibraryHit>> executeStream(
|
||||
TmdbAvailabilityReq input, {
|
||||
CancellationToken? cancelToken,
|
||||
}) {
|
||||
final controller = StreamController<List<TmdbLibraryHit>>();
|
||||
Timer? settleTimer;
|
||||
() async {
|
||||
try {
|
||||
final tmdbId = input.tmdbId.trim();
|
||||
if (tmdbId.isEmpty) {
|
||||
if (!controller.isClosed) controller.add(const []);
|
||||
return;
|
||||
}
|
||||
final isMovie = input.mediaType == 'movie';
|
||||
final includeItemTypes = isMovie ? 'Movie' : 'Series';
|
||||
final providerQuery = _buildAvailabilityProviderQuery(
|
||||
tmdbId: tmdbId,
|
||||
imdbId: isMovie ? input.imdbId : null,
|
||||
);
|
||||
|
||||
final allSessions = await sessionRepository.listAll();
|
||||
final allServers = await serverRepository.list();
|
||||
final serverMap = {for (final s in allServers) s.id: s};
|
||||
|
||||
final acc = <TmdbLibraryHit>[];
|
||||
var settled = false;
|
||||
void emit() {
|
||||
if (!controller.isClosed) controller.add(List.unmodifiable(acc));
|
||||
}
|
||||
|
||||
var pending = 0;
|
||||
final futures = <Future<void>>[];
|
||||
for (final session in allSessions) {
|
||||
final server = serverMap[session.serverId];
|
||||
if (server == null) continue;
|
||||
pending++;
|
||||
futures.add(
|
||||
_probeServer(
|
||||
server,
|
||||
session,
|
||||
includeItemTypes,
|
||||
providerQuery,
|
||||
cancelToken: cancelToken,
|
||||
).then((hit) {
|
||||
pending--;
|
||||
if (hit != null) acc.add(hit);
|
||||
if (pending == 0) {
|
||||
settleTimer?.cancel();
|
||||
if (!settled || hit != null) emit();
|
||||
} else if (settled && hit != null) {
|
||||
emit();
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (pending == 0) {
|
||||
emit();
|
||||
return;
|
||||
}
|
||||
settleTimer = Timer(settleTimeout, () {
|
||||
settled = true;
|
||||
emit();
|
||||
});
|
||||
await Future.wait(futures);
|
||||
} catch (e) {
|
||||
if (!controller.isClosed) controller.addError(e);
|
||||
} finally {
|
||||
settleTimer?.cancel();
|
||||
if (!controller.isClosed) await controller.close();
|
||||
}
|
||||
}();
|
||||
return controller.stream;
|
||||
}
|
||||
|
||||
Future<TmdbLibraryHit?> _probeServer(
|
||||
EmbyServer server,
|
||||
SessionData session,
|
||||
String includeItemTypes,
|
||||
String providerQuery, {
|
||||
CancellationToken? cancelToken,
|
||||
}) async {
|
||||
final ctx = AuthedRequestContext(
|
||||
baseUrl: server.baseUrl,
|
||||
token: session.token,
|
||||
userId: session.userId,
|
||||
);
|
||||
try {
|
||||
final items = await embyGateway.getLibraryItems(
|
||||
ctx: ctx,
|
||||
includeItemTypes: includeItemTypes,
|
||||
anyProviderIdEquals: providerQuery,
|
||||
fields: 'ProviderIds,ProductionYear,UserData',
|
||||
limit: 1,
|
||||
recursive: true,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
if (items.isEmpty) return null;
|
||||
final item = items.first;
|
||||
return TmdbLibraryHit(
|
||||
serverId: server.id,
|
||||
serverName: server.name,
|
||||
itemId: item.Id,
|
||||
itemType: item.Type ?? includeItemTypes,
|
||||
name: item.Name,
|
||||
productionYear: item.ProductionYear,
|
||||
playbackPositionTicks: item.UserData?.PlaybackPositionTicks,
|
||||
runTimeTicks: item.RunTimeTicks,
|
||||
);
|
||||
} on CancelledException {
|
||||
return null;
|
||||
} catch (e) {
|
||||
AppLogger.warn(_ucTag, 'TmdbAvailability on ${server.name} failed', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user