Initial commit

This commit is contained in:
admin1
2026-07-14 11:11:36 +08:00
commit 656499cf94
604 changed files with 119518 additions and 0 deletions
+11
View File
@@ -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
View File
@@ -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
+30
View File
@@ -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'
+185
View File
@@ -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 + FFmpegAndroid)、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、~3060 分钟)。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).
+33
View File
@@ -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
+14
View File
@@ -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
+108
View File
@@ -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 标为 compileOnlyAGP 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"))))
}
+11
View File
@@ -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.
+70
View File
@@ -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>
+24
View File
@@ -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)
}
+8
View File
@@ -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
+5
View File
@@ -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
+14
View File
@@ -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=
+26
View File
@@ -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

+29
View File
@@ -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` 再提交。
+20
View File
File diff suppressed because one or more lines are too long
+5
View File
@@ -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
+418
View File
@@ -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您要继续吗?
+90
View File
@@ -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
; 所有 DLLFlutter 引擎、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 schemesmplayer:// → 本程序(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
View File
@@ -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,
};
+82
View File
@@ -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});
}
+61
View File
@@ -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);
}
File diff suppressed because it is too large Load Diff
+78
View File
@@ -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,
};
+49
View File
@@ -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},
];
}
+325
View File
@@ -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
+50
View File
@@ -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,
};
+28
View File
@@ -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);
}
}
+10
View File
@@ -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';
+354
View File
@@ -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,
);
}
}
+830
View File
@@ -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
+41
View File
@@ -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,
};
+14
View File
@@ -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);
}
+49
View File
@@ -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
View File
@@ -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
+36
View File
@@ -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,
};
+722
View File
@@ -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>();
}
File diff suppressed because it is too large Load Diff
+250
View File
@@ -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,
};
+181
View File
@@ -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'},
],
};
}
+854
View File
@@ -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
+81
View File
@@ -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,
};
+19
View File
@@ -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;
}
+280
View File
@@ -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(),
};
}
File diff suppressed because it is too large Load Diff
+381
View File
@@ -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(),
};
+77
View File
@@ -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;
File diff suppressed because it is too large Load Diff
+66
View File
@@ -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,
};
+324
View File
@@ -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() ?? '';
File diff suppressed because it is too large Load Diff
+384
View File
@@ -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);
}
+40
View File
@@ -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);
}
}
+20
View File
@@ -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
View File
@@ -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
View File
@@ -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,
};
+22
View File
@@ -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
View File
@@ -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
View File
@@ -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};
+13
View File
@@ -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);
}
+171
View File
@@ -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,
});
}
+126
View File
@@ -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,
});
}
+14
View File
@@ -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);
}
+161
View File
@@ -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