New Year, New Video Player + Other Goodies (#593)

This commit is contained in:
Ethan Pippin 2023-04-20 09:33:51 -06:00 committed by GitHub
parent 87738af587
commit a08a92e98a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
680 changed files with 20531 additions and 16111 deletions

View File

@ -1,6 +1,7 @@
<p align="center">
<img alt="Swiftfin" height="125" src="https://github.com/jellyfin/Swiftfin/blob/main/Swiftfin/Assets.xcassets/AppIcon.appiconset/152.png">
<h2 align="center">Swiftfin</h2>
<div align="center">
<img alt="Swiftfin" src="./Resources/primary-wide.svg">
<h1>Swiftfin</h1>
<a href="https://translate.jellyfin.org/engage/swiftfin/">
<img src="https://translate.jellyfin.org/widgets/swiftfin/-/svg-badge.svg"/>
</a>
@ -10,29 +11,34 @@
<a href="https://discord.gg/zHBxVSXdBV">
<img src="https://img.shields.io/badge/Talk%20on-Discord-brightgreen">
</a>
</p>
</div>
<p align="center">
<b>Swiftfin</b> is a modern video client for the <a href="https://github.com/jellyfin/jellyfin">Jellyfin</a> media server. Redesigned in Swift to maximize direct play with the power of <b>VLC</b> and look <b>native</b> on all classes of Apple devices.
<b>Swiftfin</b> is a modern video client for the <a href="https://github.com/jellyfin/jellyfin">Jellyfin</a> media server. Made using Swift to maximize direct play with the power of <b>VLC</b> and look <b>native</b> on all classes of Apple devices.
</p>
## ⚡️ Download
**✨New! Available on the App Store**
<a href='https://apps.apple.com/ca/app/swiftfin/id1604098728'><img width='153' alt='Download on the Apple App Store' src='https://github.com/jellyfin/jellyfin.org/blob/master/static/images/store-icons/app-store.svg'/></a>
Learn more on our [announcement post](https://jellyfin.org/posts/2022/12/29/swiftfin/).
Read about the details on our [announcement post](https://jellyfin.org/posts/2022/12/29/swiftfin/).
<a href="https://apps.apple.com/us/app/swiftfin/id1604098728">
<img height=75 alt="Download on the Apple App Store" src="./Resources/Download_on_the_App_Store_Badge_US-UK_RGB_blk_092917.svg"/>
</a>
A [TestFlight](./TESTFLIGHT.md) instance is also available.
## ⚙️ Development
Thank you for your interest in Swiftfin! Please check out the [Contribution Guidelines](https://github.com/jellyfin/Swiftfin/blob/main/contributing.md) to get started.
## 📚 Translations
**Don't see Swiftfin in your language?**
Check out our [Weblate instance](https://translate.jellyfin.org/projects/swiftfin/) to help translate Swiftfin and other projects.
Check out our [Weblate instance](https://translate.jellyfin.org/projects/swiftfin/) to help translate Swiftfin and other Jellyfin projects.
<a href="https://translate.jellyfin.org/engage/swiftfin/">
<img src="https://translate.jellyfin.org/widgets/swiftfin/-/multi-auto.svg"/>
</a>
## ⚙️ Development
Thank you for your interest in Swiftfin! Please check out the [Contribution Guidelines](https://github.com/jellyfin/Swiftfin/blob/main/contributing.md) to get started.

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AppIcon-dark-blue</title>
<defs>
<linearGradient x1="19.2532352%" y1="40.8257212%" x2="100.658798%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#1F4EA7" offset="0%"></stop>
<stop stop-color="#00DDFF" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Dark" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AppIcon-dark-blue" fill-rule="nonzero">
<rect id="solid-background" fill="#000000" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="url(#linearGradient-1)"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AppIcon-dark-green</title>
<defs>
<linearGradient x1="19.2532352%" y1="40.8257212%" x2="100.658798%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#316D0C" offset="0%"></stop>
<stop stop-color="#7CD841" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Dark" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AppIcon-dark-green" fill-rule="nonzero">
<rect id="solid-background" fill="#000000" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="url(#linearGradient-1)"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AppIcon-dark-jellyfin</title>
<defs>
<linearGradient x1="19.8247286%" y1="41.1617938%" x2="100.658798%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#AA5CC3" offset="0%"></stop>
<stop stop-color="#00A4DC" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Dark" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AppIcon-dark-jellyfin" fill-rule="nonzero">
<rect id="solid-background" fill="#000000" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="url(#linearGradient-1)"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AppIcon-dark-orange</title>
<defs>
<linearGradient x1="19.2532352%" y1="40.8257212%" x2="100.658798%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#D64800" offset="0%"></stop>
<stop stop-color="#FFB657" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Dark" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AppIcon-dark-orange" fill-rule="nonzero">
<rect id="solid-background" fill="#000000" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="url(#linearGradient-1)"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AppIcon-dark-red</title>
<defs>
<linearGradient x1="19.2532352%" y1="40.8257212%" x2="100.658798%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#B42222" offset="0%"></stop>
<stop stop-color="#FF8383" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Dark" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AppIcon-dark-red" fill-rule="nonzero">
<rect id="solid-background" fill="#000000" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="url(#linearGradient-1)"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AppIcon-dark-yellow</title>
<defs>
<linearGradient x1="19.8247286%" y1="41.1617938%" x2="100.658798%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#988E0D" offset="0%"></stop>
<stop stop-color="#FFEE00" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Dark" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AppIcon-dark-yellow" fill-rule="nonzero">
<rect id="solid-background" fill="#000000" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="url(#linearGradient-1)"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AppIcon-invertedDark-blue</title>
<defs>
<linearGradient x1="18.6834859%" y1="40.8257212%" x2="101.597525%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#1F4EA7" offset="0%"></stop>
<stop stop-color="#00DDFF" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Inverted-Dark" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AppIcon-invertedDark-blue" fill-rule="nonzero">
<rect id="solid-background" fill="url(#linearGradient-1)" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="#000000"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AppIcon-invertedDark-green</title>
<defs>
<linearGradient x1="18.6834859%" y1="40.8257212%" x2="101.597525%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#316D0C" offset="0%"></stop>
<stop stop-color="#7CD841" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Inverted-Dark" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AppIcon-invertedDark-green" fill-rule="nonzero">
<rect id="solid-background" fill="url(#linearGradient-1)" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="#000000"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AppIcon-invertedDark-jellyfin</title>
<defs>
<linearGradient x1="19.2655693%" y1="41.1617938%" x2="101.597525%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#AA5CC3" offset="0%"></stop>
<stop stop-color="#00A4DC" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Inverted-Dark" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AppIcon-invertedDark-jellyfin" fill-rule="nonzero">
<rect id="solid-background" fill="url(#linearGradient-1)" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="#000000"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AppIcon-invertedDark-orange</title>
<defs>
<linearGradient x1="18.6834859%" y1="40.8257212%" x2="101.597525%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#D64800" offset="0%"></stop>
<stop stop-color="#FFB657" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Inverted-Dark" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AppIcon-invertedDark-orange" fill-rule="nonzero">
<rect id="solid-background" fill="url(#linearGradient-1)" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="#000000"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AppIcon-invertedDark-red</title>
<defs>
<linearGradient x1="18.6834859%" y1="40.8257212%" x2="101.597525%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#B42222" offset="0%"></stop>
<stop stop-color="#FF8383" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Inverted-Dark" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AppIcon-invertedDark-red" fill-rule="nonzero">
<rect id="solid-background" fill="url(#linearGradient-1)" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="#000000"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AppIcon-invertedDark-yellow</title>
<defs>
<linearGradient x1="19.2655693%" y1="41.1617938%" x2="101.597525%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#988E0D" offset="0%"></stop>
<stop stop-color="#FFEE00" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Inverted-Dark" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AppIcon-invertedDark-yellow" fill-rule="nonzero">
<rect id="solid-background" fill="url(#linearGradient-1)" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="#000000"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AppIcon-invertedLight-blue</title>
<defs>
<linearGradient x1="18.6834859%" y1="40.8257212%" x2="101.597525%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#1F4EA7" offset="0%"></stop>
<stop stop-color="#00DDFF" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Inverted-Light" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AppIcon-invertedLight-blue" fill-rule="nonzero">
<rect id="solid-background" fill="url(#linearGradient-1)" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="#FFFFFF"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AppIcon-invertedLight-green</title>
<defs>
<linearGradient x1="18.6834859%" y1="40.8257212%" x2="101.597525%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#316D0C" offset="0%"></stop>
<stop stop-color="#7CD841" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Inverted-Light" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AppIcon-invertedLight-green" fill-rule="nonzero">
<rect id="solid-background" fill="url(#linearGradient-1)" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="#FFFFFF"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AppIcon-invertedLight-jellyfin</title>
<defs>
<linearGradient x1="19.2655693%" y1="41.1617938%" x2="101.597525%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#AA5CC3" offset="0%"></stop>
<stop stop-color="#00A4DC" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Inverted-Light" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AppIcon-invertedLight-jellyfin" fill-rule="nonzero">
<rect id="solid-background" fill="url(#linearGradient-1)" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="#FFFFFF"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AppIcon-invertedLight-orange</title>
<defs>
<linearGradient x1="18.6834859%" y1="40.8257212%" x2="101.597525%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#D64800" offset="0%"></stop>
<stop stop-color="#FFB657" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Inverted-Light" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AppIcon-invertedLight-orange" fill-rule="nonzero">
<rect id="solid-background" fill="url(#linearGradient-1)" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="#FFFFFF"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AppIcon-invertedLight-red</title>
<defs>
<linearGradient x1="18.6834859%" y1="40.8257212%" x2="101.597525%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#B42222" offset="0%"></stop>
<stop stop-color="#FF8383" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Inverted-Light" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AppIcon-invertedLight-red" fill-rule="nonzero">
<rect id="solid-background" fill="url(#linearGradient-1)" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="#FFFFFF"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AppIcon-invertedLight-yellow</title>
<defs>
<linearGradient x1="19.2655693%" y1="41.1617938%" x2="101.597525%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#988E0D" offset="0%"></stop>
<stop stop-color="#FFEE00" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Inverted-Light" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AppIcon-invertedLight-yellow" fill-rule="nonzero">
<rect id="solid-background" fill="url(#linearGradient-1)" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="#FFFFFF"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AppIcon-light-blue</title>
<defs>
<linearGradient x1="19.2532352%" y1="40.8257212%" x2="100.658798%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#1F4EA7" offset="0%"></stop>
<stop stop-color="#00DDFF" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Light" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AppIcon-light-blue" fill-rule="nonzero">
<rect id="solid-background" fill="#FFFFFF" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="url(#linearGradient-1)"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AppIcon-light-green</title>
<defs>
<linearGradient x1="19.2532352%" y1="40.8257212%" x2="100.658798%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#316D0C" offset="0%"></stop>
<stop stop-color="#7CD841" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Light" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AppIcon-light-green" fill-rule="nonzero">
<rect id="solid-background" fill="#FFFFFF" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="url(#linearGradient-1)"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AppIcon-light-jellyfin</title>
<defs>
<linearGradient x1="19.8247286%" y1="41.1617938%" x2="100.658798%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#AA5CC3" offset="0%"></stop>
<stop stop-color="#00A4DC" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Light" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AppIcon-light-jellyfin" fill-rule="nonzero">
<rect id="solid-background" fill="#FFFFFF" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="url(#linearGradient-1)"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AppIcon-light-orange</title>
<defs>
<linearGradient x1="19.2532352%" y1="40.8257212%" x2="100.658798%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#D64800" offset="0%"></stop>
<stop stop-color="#FFB657" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Light" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AppIcon-light-orange" fill-rule="nonzero">
<rect id="solid-background" fill="#FFFFFF" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="url(#linearGradient-1)"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AppIcon-light-red</title>
<defs>
<linearGradient x1="19.2532352%" y1="40.8257212%" x2="100.658798%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#B42222" offset="0%"></stop>
<stop stop-color="#FF8383" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Light" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AppIcon-light-red" fill-rule="nonzero">
<rect id="solid-background" fill="#FFFFFF" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="url(#linearGradient-1)"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>AppIcon-light-yellow</title>
<defs>
<linearGradient x1="19.8247286%" y1="41.1617938%" x2="100.658798%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#988E0D" offset="0%"></stop>
<stop stop-color="#FFEE00" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Light" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="AppIcon-light-yellow" fill-rule="nonzero">
<rect id="solid-background" fill="#FFFFFF" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="url(#linearGradient-1)"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>primary</title>
<defs>
<linearGradient x1="19.2532352%" y1="40.8257212%" x2="100.658798%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#1F4EA7" offset="0%"></stop>
<stop stop-color="#00DDFF" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Primary" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="primary" fill-rule="nonzero">
<rect id="solid-background" fill="#020B23" x="0" y="0" width="1024" height="1024"></rect>
<path d="M511.582082,136 C609.711866,136 924.91518,714.816848 877.299462,811.251498 C829.683744,907.686149 194.101496,908.81122 145.960253,811.251498 C97.8190104,713.691777 413.547848,136 511.582082,136 Z M511.677632,288.155769 C447.420301,288.155769 240.55469,666.677846 272.102093,730.597947 C303.649497,794.518049 720.072042,793.810861 751.269096,730.597947 C782.46615,667.368961 575.934964,288.155769 511.677632,288.155769 Z M511.109544,425.091384 C543.724099,425.107443 648.323295,617.009305 632.517902,648.998301 C616.71251,680.987298 405.681839,681.34059 389.701185,648.998301 C373.720531,616.656013 478.542787,425.091384 511.109544,425.091384 Z" id="Combined-Shape" fill="url(#linearGradient-1)"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,46 @@
<svg id="livetype" xmlns="http://www.w3.org/2000/svg" width="119.66407" height="40" viewBox="0 0 119.66407 40">
<title>Download_on_the_App_Store_Badge_US-UK_RGB_blk_4SVG_092917</title>
<g>
<g>
<g>
<path d="M110.13477,0H9.53468c-.3667,0-.729,0-1.09473.002-.30615.002-.60986.00781-.91895.0127A13.21476,13.21476,0,0,0,5.5171.19141a6.66509,6.66509,0,0,0-1.90088.627A6.43779,6.43779,0,0,0,1.99757,1.99707,6.25844,6.25844,0,0,0,.81935,3.61816a6.60119,6.60119,0,0,0-.625,1.90332,12.993,12.993,0,0,0-.1792,2.002C.00587,7.83008.00489,8.1377,0,8.44434V31.5586c.00489.3105.00587.6113.01515.9219a12.99232,12.99232,0,0,0,.1792,2.0019,6.58756,6.58756,0,0,0,.625,1.9043A6.20778,6.20778,0,0,0,1.99757,38.001a6.27445,6.27445,0,0,0,1.61865,1.1787,6.70082,6.70082,0,0,0,1.90088.6308,13.45514,13.45514,0,0,0,2.0039.1768c.30909.0068.6128.0107.91895.0107C8.80567,40,9.168,40,9.53468,40H110.13477c.3594,0,.7246,0,1.084-.002.3047,0,.6172-.0039.9219-.0107a13.279,13.279,0,0,0,2-.1768,6.80432,6.80432,0,0,0,1.9082-.6308,6.27742,6.27742,0,0,0,1.6172-1.1787,6.39482,6.39482,0,0,0,1.1816-1.6143,6.60413,6.60413,0,0,0,.6191-1.9043,13.50643,13.50643,0,0,0,.1856-2.0019c.0039-.3106.0039-.6114.0039-.9219.0078-.3633.0078-.7246.0078-1.0938V9.53613c0-.36621,0-.72949-.0078-1.09179,0-.30664,0-.61426-.0039-.9209a13.5071,13.5071,0,0,0-.1856-2.002,6.6177,6.6177,0,0,0-.6191-1.90332,6.46619,6.46619,0,0,0-2.7988-2.7998,6.76754,6.76754,0,0,0-1.9082-.627,13.04394,13.04394,0,0,0-2-.17676c-.3047-.00488-.6172-.01074-.9219-.01269-.3594-.002-.7246-.002-1.084-.002Z" style="fill: #a6a6a6"/>
<path d="M8.44483,39.125c-.30468,0-.602-.0039-.90429-.0107a12.68714,12.68714,0,0,1-1.86914-.1631,5.88381,5.88381,0,0,1-1.65674-.5479,5.40573,5.40573,0,0,1-1.397-1.0166,5.32082,5.32082,0,0,1-1.02051-1.3965,5.72186,5.72186,0,0,1-.543-1.6572,12.41351,12.41351,0,0,1-.1665-1.875c-.00634-.2109-.01464-.9131-.01464-.9131V8.44434S.88185,7.75293.8877,7.5498a12.37039,12.37039,0,0,1,.16553-1.87207,5.7555,5.7555,0,0,1,.54346-1.6621A5.37349,5.37349,0,0,1,2.61183,2.61768,5.56543,5.56543,0,0,1,4.01417,1.59521a5.82309,5.82309,0,0,1,1.65332-.54394A12.58589,12.58589,0,0,1,7.543.88721L8.44532.875H111.21387l.9131.0127a12.38493,12.38493,0,0,1,1.8584.16259,5.93833,5.93833,0,0,1,1.6709.54785,5.59374,5.59374,0,0,1,2.415,2.41993,5.76267,5.76267,0,0,1,.5352,1.64892,12.995,12.995,0,0,1,.1738,1.88721c.0029.2832.0029.5874.0029.89014.0079.375.0079.73193.0079,1.09179V30.4648c0,.3633,0,.7178-.0079,1.0752,0,.3252,0,.6231-.0039.9297a12.73126,12.73126,0,0,1-.1709,1.8535,5.739,5.739,0,0,1-.54,1.67,5.48029,5.48029,0,0,1-1.0156,1.3857,5.4129,5.4129,0,0,1-1.3994,1.0225,5.86168,5.86168,0,0,1-1.668.5498,12.54218,12.54218,0,0,1-1.8692.1631c-.2929.0068-.5996.0107-.8974.0107l-1.084.002Z"/>
</g>
<g id="_Group_" data-name="&lt;Group&gt;">
<g id="_Group_2" data-name="&lt;Group&gt;">
<g id="_Group_3" data-name="&lt;Group&gt;">
<path id="_Path_" data-name="&lt;Path&gt;" d="M24.76888,20.30068a4.94881,4.94881,0,0,1,2.35656-4.15206,5.06566,5.06566,0,0,0-3.99116-2.15768c-1.67924-.17626-3.30719,1.00483-4.1629,1.00483-.87227,0-2.18977-.98733-3.6085-.95814a5.31529,5.31529,0,0,0-4.47292,2.72787c-1.934,3.34842-.49141,8.26947,1.3612,10.97608.9269,1.32535,2.01018,2.8058,3.42763,2.7533,1.38706-.05753,1.9051-.88448,3.5794-.88448,1.65876,0,2.14479.88448,3.591.8511,1.48838-.02416,2.42613-1.33124,3.32051-2.66914a10.962,10.962,0,0,0,1.51842-3.09251A4.78205,4.78205,0,0,1,24.76888,20.30068Z" style="fill: #fff"/>
<path id="_Path_2" data-name="&lt;Path&gt;" d="M22.03725,12.21089a4.87248,4.87248,0,0,0,1.11452-3.49062,4.95746,4.95746,0,0,0-3.20758,1.65961,4.63634,4.63634,0,0,0-1.14371,3.36139A4.09905,4.09905,0,0,0,22.03725,12.21089Z" style="fill: #fff"/>
</g>
</g>
<g>
<path d="M42.30227,27.13965h-4.7334l-1.13672,3.35645H34.42727l4.4834-12.418h2.083l4.4834,12.418H43.438ZM38.0591,25.59082h3.752l-1.84961-5.44727h-.05176Z" style="fill: #fff"/>
<path d="M55.15969,25.96973c0,2.81348-1.50586,4.62109-3.77832,4.62109a3.0693,3.0693,0,0,1-2.84863-1.584h-.043v4.48438h-1.8584V21.44238H48.4302v1.50586h.03418a3.21162,3.21162,0,0,1,2.88281-1.60059C53.645,21.34766,55.15969,23.16406,55.15969,25.96973Zm-1.91016,0c0-1.833-.94727-3.03809-2.39258-3.03809-1.41992,0-2.375,1.23047-2.375,3.03809,0,1.82422.95508,3.0459,2.375,3.0459C52.30227,29.01563,53.24953,27.81934,53.24953,25.96973Z" style="fill: #fff"/>
<path d="M65.12453,25.96973c0,2.81348-1.50586,4.62109-3.77832,4.62109a3.0693,3.0693,0,0,1-2.84863-1.584h-.043v4.48438h-1.8584V21.44238H58.395v1.50586h.03418A3.21162,3.21162,0,0,1,61.312,21.34766C63.60988,21.34766,65.12453,23.16406,65.12453,25.96973Zm-1.91016,0c0-1.833-.94727-3.03809-2.39258-3.03809-1.41992,0-2.375,1.23047-2.375,3.03809,0,1.82422.95508,3.0459,2.375,3.0459C62.26711,29.01563,63.21438,27.81934,63.21438,25.96973Z" style="fill: #fff"/>
<path d="M71.71047,27.03613c.1377,1.23145,1.334,2.04,2.96875,2.04,1.56641,0,2.69336-.80859,2.69336-1.91895,0-.96387-.67969-1.541-2.28906-1.93652l-1.60937-.3877c-2.28027-.55078-3.33887-1.61719-3.33887-3.34766,0-2.14258,1.86719-3.61426,4.51855-3.61426,2.624,0,4.42285,1.47168,4.4834,3.61426h-1.876c-.1123-1.23926-1.13672-1.9873-2.63379-1.9873s-2.52148.75684-2.52148,1.8584c0,.87793.6543,1.39453,2.25488,1.79l1.36816.33594c2.54785.60254,3.60645,1.626,3.60645,3.44238,0,2.32324-1.85059,3.77832-4.79395,3.77832-2.75391,0-4.61328-1.4209-4.7334-3.667Z" style="fill: #fff"/>
<path d="M83.34621,19.2998v2.14258h1.72168v1.47168H83.34621v4.99121c0,.77539.34473,1.13672,1.10156,1.13672a5.80752,5.80752,0,0,0,.61133-.043v1.46289a5.10351,5.10351,0,0,1-1.03223.08594c-1.833,0-2.54785-.68848-2.54785-2.44434V22.91406H80.16262V21.44238H81.479V19.2998Z" style="fill: #fff"/>
<path d="M86.065,25.96973c0-2.84863,1.67773-4.63867,4.29395-4.63867,2.625,0,4.29492,1.79,4.29492,4.63867,0,2.85645-1.66113,4.63867-4.29492,4.63867C87.72609,30.6084,86.065,28.82617,86.065,25.96973Zm6.69531,0c0-1.9541-.89551-3.10742-2.40137-3.10742s-2.40039,1.16211-2.40039,3.10742c0,1.96191.89453,3.10645,2.40039,3.10645S92.76027,27.93164,92.76027,25.96973Z" style="fill: #fff"/>
<path d="M96.18606,21.44238h1.77246v1.541h.043a2.1594,2.1594,0,0,1,2.17773-1.63574,2.86616,2.86616,0,0,1,.63672.06934v1.73828a2.59794,2.59794,0,0,0-.835-.1123,1.87264,1.87264,0,0,0-1.93652,2.083v5.37012h-1.8584Z" style="fill: #fff"/>
<path d="M109.3843,27.83691c-.25,1.64355-1.85059,2.77148-3.89844,2.77148-2.63379,0-4.26855-1.76465-4.26855-4.5957,0-2.83984,1.64355-4.68164,4.19043-4.68164,2.50488,0,4.08008,1.7207,4.08008,4.46582v.63672h-6.39453v.1123a2.358,2.358,0,0,0,2.43555,2.56445,2.04834,2.04834,0,0,0,2.09082-1.27344Zm-6.28223-2.70215h4.52637a2.1773,2.1773,0,0,0-2.2207-2.29785A2.292,2.292,0,0,0,103.10207,25.13477Z" style="fill: #fff"/>
</g>
</g>
</g>
<g id="_Group_4" data-name="&lt;Group&gt;">
<g>
<path d="M37.82619,8.731a2.63964,2.63964,0,0,1,2.80762,2.96484c0,1.90625-1.03027,3.002-2.80762,3.002H35.67092V8.731Zm-1.22852,5.123h1.125a1.87588,1.87588,0,0,0,1.96777-2.146,1.881,1.881,0,0,0-1.96777-2.13379h-1.125Z" style="fill: #fff"/>
<path d="M41.68068,12.44434a2.13323,2.13323,0,1,1,4.24707,0,2.13358,2.13358,0,1,1-4.24707,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C44.57522,13.99463,45.01369,13.42432,45.01369,12.44434Z" style="fill: #fff"/>
<path d="M51.57326,14.69775h-.92187l-.93066-3.31641h-.07031l-.92676,3.31641h-.91309l-1.24121-4.50293h.90137l.80664,3.436h.06641l.92578-3.436h.85254l.92578,3.436h.07031l.80273-3.436h.88867Z" style="fill: #fff"/>
<path d="M53.85354,10.19482H54.709v.71533h.06641a1.348,1.348,0,0,1,1.34375-.80225,1.46456,1.46456,0,0,1,1.55859,1.6748v2.915h-.88867V12.00586c0-.72363-.31445-1.0835-.97168-1.0835a1.03294,1.03294,0,0,0-1.0752,1.14111v2.63428h-.88867Z" style="fill: #fff"/>
<path d="M59.09377,8.437h.88867v6.26074h-.88867Z" style="fill: #fff"/>
<path d="M61.21779,12.44434a2.13346,2.13346,0,1,1,4.24756,0,2.1338,2.1338,0,1,1-4.24756,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C64.11232,13.99463,64.5508,13.42432,64.5508,12.44434Z" style="fill: #fff"/>
<path d="M66.4009,13.42432c0-.81055.60352-1.27783,1.6748-1.34424l1.21973-.07031v-.38867c0-.47559-.31445-.74414-.92187-.74414-.49609,0-.83984.18213-.93848.50049h-.86035c.09082-.77344.81836-1.26953,1.83984-1.26953,1.12891,0,1.76563.562,1.76563,1.51318v3.07666h-.85547v-.63281h-.07031a1.515,1.515,0,0,1-1.35254.707A1.36026,1.36026,0,0,1,66.4009,13.42432Zm2.89453-.38477v-.37646l-1.09961.07031c-.62012.0415-.90137.25244-.90137.64941,0,.40527.35156.64111.835.64111A1.0615,1.0615,0,0,0,69.29543,13.03955Z" style="fill: #fff"/>
<path d="M71.34816,12.44434c0-1.42285.73145-2.32422,1.86914-2.32422a1.484,1.484,0,0,1,1.38086.79h.06641V8.437h.88867v6.26074h-.85156v-.71143h-.07031a1.56284,1.56284,0,0,1-1.41406.78564C72.0718,14.772,71.34816,13.87061,71.34816,12.44434Zm.918,0c0,.95508.4502,1.52979,1.20313,1.52979.749,0,1.21191-.583,1.21191-1.52588,0-.93848-.46777-1.52979-1.21191-1.52979C72.72121,10.91846,72.26613,11.49707,72.26613,12.44434Z" style="fill: #fff"/>
<path d="M79.23,12.44434a2.13323,2.13323,0,1,1,4.24707,0,2.13358,2.13358,0,1,1-4.24707,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C82.12453,13.99463,82.563,13.42432,82.563,12.44434Z" style="fill: #fff"/>
<path d="M84.66945,10.19482h.85547v.71533h.06641a1.348,1.348,0,0,1,1.34375-.80225,1.46456,1.46456,0,0,1,1.55859,1.6748v2.915H87.605V12.00586c0-.72363-.31445-1.0835-.97168-1.0835a1.03294,1.03294,0,0,0-1.0752,1.14111v2.63428h-.88867Z" style="fill: #fff"/>
<path d="M93.51516,9.07373v1.1416h.97559v.74854h-.97559V13.2793c0,.47168.19434.67822.63672.67822a2.96657,2.96657,0,0,0,.33887-.02051v.74023a2.9155,2.9155,0,0,1-.4834.04541c-.98828,0-1.38184-.34766-1.38184-1.21582v-2.543h-.71484v-.74854h.71484V9.07373Z" style="fill: #fff"/>
<path d="M95.70461,8.437h.88086v2.48145h.07031a1.3856,1.3856,0,0,1,1.373-.80664,1.48339,1.48339,0,0,1,1.55078,1.67871v2.90723H98.69v-2.688c0-.71924-.335-1.0835-.96289-1.0835a1.05194,1.05194,0,0,0-1.13379,1.1416v2.62988h-.88867Z" style="fill: #fff"/>
<path d="M104.76125,13.48193a1.828,1.828,0,0,1-1.95117,1.30273A2.04531,2.04531,0,0,1,100.73,12.46045a2.07685,2.07685,0,0,1,2.07617-2.35254c1.25293,0,2.00879.856,2.00879,2.27V12.688h-3.17969v.0498a1.1902,1.1902,0,0,0,1.19922,1.29,1.07934,1.07934,0,0,0,1.07129-.5459Zm-3.126-1.45117h2.27441a1.08647,1.08647,0,0,0-1.1084-1.1665A1.15162,1.15162,0,0,0,101.63527,12.03076Z" style="fill: #fff"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1000px" height="200px" viewBox="0 0 1000 200" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>primary-wide</title>
<defs>
<linearGradient x1="19.1597758%" y1="40.8257212%" x2="100.812783%" y2="88.6971024%" id="linearGradient-1">
<stop stop-color="#1F4EA7" offset="0%"></stop>
<stop stop-color="#00DDFF" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Primary" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="primary-wide" fill-rule="nonzero">
<rect id="solid-background" fill="#020B23" x="0" y="0" width="1000" height="200" rx="20"></rect>
<path d="M500.518106,34.9672897 C517.730037,34.9672897 573.016591,136.33762 564.66481,153.226576 C556.313029,170.115532 444.832119,170.31257 436.388161,153.226576 C427.944204,136.140582 483.322935,34.9672897 500.518106,34.9672897 Z M500.534866,61.6148909 C489.264151,61.6148909 452.979993,127.906859 458.513397,139.101422 C464.046801,150.295985 537.087174,150.172133 542.559127,139.101422 C548.03108,128.027897 511.80558,61.6148909 500.534866,61.6148909 Z M500.435223,85.5969305 C506.155805,85.5997429 524.502468,119.208224 521.730208,124.810575 C518.957947,130.412926 481.943239,130.4748 479.140238,124.810575 C476.337237,119.146351 494.723025,85.5969305 500.435223,85.5969305 Z" id="Combined-Shape" fill="url(#linearGradient-1)"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,40 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Foundation
import UIKit
protocol AppIcon: CaseIterable, Identifiable, Displayable, RawRepresentable {
var iconName: String { get }
var iconPreview: UIImage { get }
static var tag: String { get }
static func createCase(iconName: String) -> Self?
}
extension AppIcon where ID == String, RawValue == String {
var iconName: String {
"AppIcon-\(Self.tag)-\(rawValue)"
}
var iconPreview: UIImage {
UIImage(named: iconName) ?? UIImage()
}
var id: String {
iconName
}
static func createCase(iconName: String) -> Self? {
let split = iconName.split(separator: "-")
guard split.count == 3, split[1] == Self.tag else { return nil }
return Self(rawValue: String(split[2]))
}
}

View File

@ -0,0 +1,38 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Foundation
enum DarkAppIcon: String, AppIcon {
case blue
case green
case orange
case red
case yellow
case jellyfin
var displayTitle: String {
switch self {
case .blue:
return L10n.blue
case .green:
return L10n.green
case .orange:
return L10n.orange
case .red:
return L10n.red
case .yellow:
return L10n.yellow
case .jellyfin:
return "Jellyfin"
}
}
static let tag: String = "dark"
}

View File

@ -0,0 +1,38 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Foundation
enum InvertedDarkAppIcon: String, AppIcon {
case blue
case green
case orange
case red
case yellow
case jellyfin
var displayTitle: String {
switch self {
case .blue:
return L10n.blue
case .green:
return L10n.green
case .orange:
return L10n.orange
case .red:
return L10n.red
case .yellow:
return L10n.yellow
case .jellyfin:
return "Jellyfin"
}
}
static let tag: String = "invertedDark"
}

View File

@ -0,0 +1,38 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Foundation
enum InvertedLightAppIcon: String, AppIcon {
case blue
case green
case orange
case red
case yellow
case jellyfin
var displayTitle: String {
switch self {
case .blue:
return L10n.blue
case .green:
return L10n.green
case .orange:
return L10n.orange
case .red:
return L10n.red
case .yellow:
return L10n.yellow
case .jellyfin:
return "Jellyfin"
}
}
static let tag: String = "invertedLight"
}

View File

@ -0,0 +1,38 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Foundation
enum LightAppIcon: String, AppIcon {
case blue
case green
case orange
case red
case yellow
case jellyfin
var displayTitle: String {
switch self {
case .blue:
return L10n.blue
case .green:
return L10n.green
case .orange:
return L10n.orange
case .red:
return L10n.red
case .yellow:
return L10n.yellow
case .jellyfin:
return "Jellyfin"
}
}
static let tag: String = "light"
}

View File

@ -0,0 +1,23 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Foundation
enum PrimaryAppIcon: String, AppIcon {
case primary
var displayTitle: String {
switch self {
case .primary:
return L10n.primary
}
}
static let tag: String = "primary"
}

View File

@ -3,10 +3,10 @@
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Foundation
import PulseUI
import Stinsen
import SwiftUI
@ -16,16 +16,46 @@ final class BasicAppSettingsCoordinator: NavigationCoordinatable {
@Root
var start = makeStart
#if os(iOS)
@Route(.push)
var about = makeAbout
@Route(.push)
var appIconSelector = makeAppIconSelector
@Route(.push)
var log = makeLog
#endif
#if os(tvOS)
@Route(.modal)
var log = makeLog
#endif
private let viewModel: SettingsViewModel
init() {
viewModel = .init()
}
#if os(iOS)
@ViewBuilder
func makeAbout() -> some View {
AboutAppView()
AboutAppView(viewModel: viewModel)
}
@ViewBuilder
func makeAppIconSelector() -> some View {
AppIconSelectorView(viewModel: viewModel)
}
#endif
@ViewBuilder
func makeLog() -> some View {
ConsoleView()
}
@ViewBuilder
func makeStart() -> some View {
BasicAppSettingsView(viewModel: BasicAppSettingsViewModel())
BasicAppSettingsView(viewModel: viewModel)
}
}

View File

@ -3,7 +3,7 @@
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Foundation
@ -24,10 +24,20 @@ final class BasicLibraryCoordinator: NavigationCoordinatable {
@Root
var start = makeStart
#if os(iOS)
@Route(.push)
var item = makeItem
@Route(.push)
var library = makeLibrary
#endif
#if os(tvOS)
@Route(.modal)
var item = makeItem
@Route(.modal)
var library = makeLibrary
#endif
private let parameters: Parameters
@ -38,7 +48,7 @@ final class BasicLibraryCoordinator: NavigationCoordinatable {
@ViewBuilder
func makeStart() -> some View {
BasicLibraryView(viewModel: parameters.viewModel)
#if !os(tvOS)
#if os(iOS)
.if(parameters.title != nil) { view in
view.navigationTitle(parameters.title ?? .emptyDash)
}

View File

@ -0,0 +1,30 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Stinsen
import SwiftUI
/// Basic coordinator to wrap a view for the purpose of being wrapped in a NavigationViewCoordinator
final class BasicNavigationViewCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \BasicNavigationViewCoordinator.start)
@Root
var start = makeStart
private let content: () -> any View
init(@ViewBuilder _ content: @escaping () -> any View) {
self.content = content
}
@ViewBuilder
private func makeStart() -> some View {
content().eraseToAnyView()
}
}

View File

@ -3,7 +3,7 @@
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Foundation

View File

@ -3,7 +3,7 @@
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Foundation

View File

@ -0,0 +1,32 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
#if os(iOS)
import Foundation
import Stinsen
import SwiftUI
final class DownloadListCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \DownloadListCoordinator.start)
@Root
var start = makeStart
@Route(.modal)
var downloadTask = makeDownloadTask
func makeDownloadTask(downloadTask: DownloadTask) -> NavigationViewCoordinator<DownloadTaskCoordinator> {
NavigationViewCoordinator(DownloadTaskCoordinator(downloadTask: downloadTask))
}
@ViewBuilder
private func makeStart() -> DownloadListView {
DownloadListView(viewModel: .init())
}
}
#endif

View File

@ -0,0 +1,32 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
#if os(iOS)
import Foundation
import Stinsen
import SwiftUI
final class DownloadTaskCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \DownloadTaskCoordinator.start)
@Root
var start = makeStart
let downloadTask: DownloadTask
init(downloadTask: DownloadTask) {
self.downloadTask = downloadTask
}
@ViewBuilder
private func makeStart() -> DownloadTaskView {
DownloadTaskView(downloadTask: downloadTask)
}
}
#endif

View File

@ -3,7 +3,7 @@
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Foundation

View File

@ -3,7 +3,7 @@
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Foundation

View File

@ -3,7 +3,7 @@
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Foundation
@ -27,8 +27,18 @@ final class ItemCoordinator: NavigationCoordinatable {
var castAndCrew = makeCastAndCrew
@Route(.modal)
var itemOverview = makeItemOverview
#if os(iOS)
@Route(.modal)
var mediaSourceInfo = makeMediaSourceInfo
@Route(.modal)
var downloadTask = makeDownloadTask
#endif
#if os(tvOS)
@Route(.fullScreen)
var videoPlayer = makeVideoPlayer
#endif
let itemDto: BaseItemDto
@ -56,10 +66,22 @@ final class ItemCoordinator: NavigationCoordinatable {
NavigationViewCoordinator(ItemOverviewCoordinator(item: itemDto))
}
func makeVideoPlayer(viewModel: VideoPlayerViewModel) -> NavigationViewCoordinator<VideoPlayerCoordinator> {
NavigationViewCoordinator(VideoPlayerCoordinator(viewModel: viewModel))
#if os(iOS)
func makeMediaSourceInfo(mediaSourceInfo: MediaSourceInfo) -> NavigationViewCoordinator<MediaSourceInfoCoordinator> {
NavigationViewCoordinator(MediaSourceInfoCoordinator(mediaSourceInfo: mediaSourceInfo))
}
func makeDownloadTask(downloadTask: DownloadTask) -> NavigationViewCoordinator<DownloadTaskCoordinator> {
NavigationViewCoordinator(DownloadTaskCoordinator(downloadTask: downloadTask))
}
#endif
#if os(tvOS)
func makeVideoPlayer(manager: VideoPlayerManager) -> NavigationViewCoordinator<VideoPlayerCoordinator> {
NavigationViewCoordinator(VideoPlayerCoordinator(manager: manager))
}
#endif
@ViewBuilder
func makeStart() -> some View {
ItemView(item: itemDto)

View File

@ -3,7 +3,7 @@
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import JellyfinAPI

View File

@ -3,7 +3,7 @@
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Defaults

View File

@ -3,46 +3,47 @@
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Defaults
import Foundation
import JellyfinAPI
import Stinsen
import SwiftUI
final class LiveTVChannelsCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \LiveTVChannelsCoordinator.start)
@Root
var start = makeStart
@Route(.modal)
var modalItem = makeModalItem
#if os(tvOS)
@Route(.fullScreen)
var videoPlayer = makeVideoPlayer
#endif
func makeModalItem(item: BaseItemDto) -> NavigationViewCoordinator<ItemCoordinator> {
NavigationViewCoordinator(ItemCoordinator(item: item))
}
func makeVideoPlayer(viewModel: VideoPlayerViewModel) -> NavigationViewCoordinator<LiveTVVideoPlayerCoordinator> {
NavigationViewCoordinator(LiveTVVideoPlayerCoordinator(viewModel: viewModel))
#if os(tvOS)
func makeVideoPlayer(manager: VideoPlayerManager) -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
BasicNavigationViewCoordinator {
Group {
if Defaults[.VideoPlayer.videoPlayerType] == .swiftfin {
VideoPlayer(manager: manager)
.overlay {
VideoPlayer.Overlay()
}
} else {
NativeVideoPlayer(manager: manager)
}
}
}
.inNavigationViewCoordinator()
}
#endif
@ViewBuilder
func makeStart() -> some View {
LiveTVChannelsView()
}
}
final class EmptyViewCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \EmptyViewCoordinator.start)
@Root
var start = makeStart
@ViewBuilder
func makeStart() -> some View {
Text("Empty")
}
}

View File

@ -3,7 +3,7 @@
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Foundation
@ -12,19 +12,14 @@ import Stinsen
import SwiftUI
final class LiveTVCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \LiveTVCoordinator.start)
@Root
var start = makeStart
@Route(.fullScreen)
var videoPlayer = makeVideoPlayer
@ViewBuilder
func makeStart() -> some View {
LiveTVChannelsView()
}
func makeVideoPlayer(viewModel: VideoPlayerViewModel) -> NavigationViewCoordinator<LiveTVVideoPlayerCoordinator> {
NavigationViewCoordinator(LiveTVVideoPlayerCoordinator(viewModel: viewModel))
}
}

View File

@ -3,9 +3,11 @@
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Algorithms
import Defaults
import Foundation
import JellyfinAPI
import Stinsen
@ -17,15 +19,47 @@ final class LiveTVProgramsCoordinator: NavigationCoordinatable {
@Root
var start = makeStart
#if os(tvOS)
@Route(.fullScreen)
var videoPlayer = makeVideoPlayer
#endif
func makeVideoPlayer(viewModel: VideoPlayerViewModel) -> NavigationViewCoordinator<LiveTVVideoPlayerCoordinator> {
NavigationViewCoordinator(LiveTVVideoPlayerCoordinator(viewModel: viewModel))
#if os(tvOS)
func makeVideoPlayer(manager: VideoPlayerManager) -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
BasicNavigationViewCoordinator {
Group {
if Defaults[.VideoPlayer.videoPlayerType] == .swiftfin {
VideoPlayer(manager: manager)
.overlay {
VideoPlayer.Overlay()
}
} else {
NativeVideoPlayer(manager: manager)
}
}
}
.inNavigationViewCoordinator()
}
#endif
@ViewBuilder
// @ViewBuilder
func makeStart() -> some View {
LiveTVProgramsView()
let viewModel = LiveTVProgramsViewModel()
let channels = (1 ..< 20).map { _ in BaseItemDto.randomItem() }
channels.forEach { channel in
viewModel.channels[channel.id!] = channel
}
viewModel.recommendedItems = channels.randomSample(count: 5)
viewModel.seriesItems = channels.randomSample(count: 5)
viewModel.movieItems = channels.randomSample(count: 5)
viewModel.sportsItems = channels.randomSample(count: 5)
viewModel.kidsItems = channels.randomSample(count: 5)
viewModel.newsItems = channels.randomSample(count: 5)
return LiveTVProgramsView(viewModel: viewModel)
}
}

View File

@ -3,7 +3,7 @@
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Foundation
@ -11,6 +11,7 @@ import Stinsen
import SwiftUI
final class LiveTVTabCoordinator: TabCoordinatable {
var child = TabChild(startingItems: [
\LiveTVTabCoordinator.programs,
\LiveTVTabCoordinator.channels,

View File

@ -3,13 +3,14 @@
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Combine
import Defaults
import Factory
import Foundation
import JellyfinAPI
import Nuke
import Stinsen
import SwiftUI
@ -26,14 +27,17 @@ final class MainCoordinator: NavigationCoordinatable {
var mainTab = makeMainTab
@Root
var serverList = makeServerList
@Route(.fullScreen)
var videoPlayer = makeVideoPlayer
private var cancellables = Set<AnyCancellable>()
init() {
if SessionManager.main.currentLogin != nil {
self.stack = NavigationStack(initial: \MainCoordinator.mainTab)
if Container.userSession.callAsFunction().authenticated {
stack = NavigationStack(initial: \MainCoordinator.mainTab)
} else {
self.stack = NavigationStack(initial: \MainCoordinator.serverList)
stack = NavigationStack(initial: \MainCoordinator.serverList)
}
ImageCache.shared.costLimit = 125 * 1024 * 1024 // 125MB memory
@ -42,25 +46,11 @@ final class MainCoordinator: NavigationCoordinatable {
WidgetCenter.shared.reloadAllTimelines()
UIScrollView.appearance().keyboardDismissMode = .onDrag
// Back bar button item setup
let config = UIImage.SymbolConfiguration(paletteColors: [.white, .jellyfinPurple])
let backButtonBackgroundImage = UIImage(systemName: "chevron.backward.circle.fill", withConfiguration: config)
let barAppearance = UINavigationBar.appearance()
barAppearance.backIndicatorImage = backButtonBackgroundImage
barAppearance.backIndicatorTransitionMaskImage = backButtonBackgroundImage
barAppearance.tintColor = UIColor(Color.jellyfinPurple)
// Notification setup for state
Notifications[.didSignIn].subscribe(self, selector: #selector(didSignIn))
Notifications[.didSignOut].subscribe(self, selector: #selector(didSignOut))
Notifications[.processDeepLink].subscribe(self, selector: #selector(processDeepLink(_:)))
Notifications[.didChangeServerCurrentURI].subscribe(self, selector: #selector(didChangeServerCurrentURI(_:)))
Defaults.publisher(.appAppearance)
.sink { _ in
JellyfinPlayerApp.setupAppearance()
}
.store(in: &cancellables)
}
@objc
@ -91,12 +81,12 @@ final class MainCoordinator: NavigationCoordinatable {
@objc
func didChangeServerCurrentURI(_ notification: Notification) {
guard let newCurrentServerState = notification.object as? SwiftfinStore.State.Server
else { fatalError("Need to have new current login state server") }
guard SessionManager.main.currentLogin != nil else { return }
if newCurrentServerState.id == SessionManager.main.currentLogin.server.id {
SessionManager.main.signInUser(server: newCurrentServerState, user: SessionManager.main.currentLogin.user)
}
// guard let newCurrentServerState = notification.object as? SwiftfinStore.State.Server
// else { fatalError("Need to have new current login state server") }
// guard SessionManager.main.currentLogin != nil else { return }
// if newCurrentServerState.id == SessionManager.main.currentLogin.server.id {
// SessionManager.main.signInUser(server: newCurrentServerState, user: SessionManager.main.currentLogin.user)
// }
}
func makeMainTab() -> MainTabCoordinator {
@ -106,4 +96,8 @@ final class MainCoordinator: NavigationCoordinatable {
func makeServerList() -> NavigationViewCoordinator<ServerListCoordinator> {
NavigationViewCoordinator(ServerListCoordinator())
}
func makeVideoPlayer(manager: VideoPlayerManager) -> VideoPlayerCoordinator {
VideoPlayerCoordinator(manager: manager)
}
}

View File

@ -3,7 +3,7 @@
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Foundation
@ -11,6 +11,7 @@ import Stinsen
import SwiftUI
final class MainTabCoordinator: TabCoordinatable {
var child = TabChild(startingItems: [
\MainTabCoordinator.home,
\MainTabCoordinator.search,

View File

@ -3,7 +3,7 @@
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Factory
@ -17,7 +17,7 @@ final class MainCoordinator: NavigationCoordinatable {
@Injected(LogManager.service)
private var logger
var stack = Stinsen.NavigationStack<MainCoordinator>(initial: \MainCoordinator.mainTab)
var stack: Stinsen.NavigationStack<MainCoordinator>
@Root
var mainTab = makeMainTab
@ -25,12 +25,15 @@ final class MainCoordinator: NavigationCoordinatable {
var serverList = makeServerList
@Root
var liveTV = makeLiveTV
// @Route(.fullScreen)
// var videoPlayer = makeVideoPlayer
init() {
if SessionManager.main.currentLogin != nil {
self.stack = NavigationStack(initial: \MainCoordinator.mainTab)
if Container.userSession.callAsFunction().authenticated {
stack = NavigationStack(initial: \MainCoordinator.mainTab)
} else {
self.stack = NavigationStack(initial: \MainCoordinator.serverList)
stack = NavigationStack(initial: \MainCoordinator.serverList)
}
ImageCache.shared.costLimit = 125 * 1024 * 1024 // 125MB memory
@ -66,4 +69,8 @@ final class MainCoordinator: NavigationCoordinatable {
func makeLiveTV() -> LiveTVTabCoordinator {
LiveTVTabCoordinator()
}
// func makeVideoPlayer(parameters: VideoPlayerCoordinator.Parameters) -> NavigationViewCoordinator<VideoPlayerCoordinator> {
// NavigationViewCoordinator(VideoPlayerCoordinator(parameters: parameters))
// }
}

View File

@ -3,7 +3,7 @@
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Foundation

View File

@ -3,7 +3,7 @@
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Foundation
@ -24,6 +24,8 @@ final class MediaCoordinator: NavigationCoordinatable {
var library = makeLibrary
@Route(.push)
var liveTV = makeLiveTV
@Route(.push)
var downloads = makeDownloads
#endif
#if os(tvOS)
@ -39,6 +41,10 @@ final class MediaCoordinator: NavigationCoordinatable {
func makeLiveTV() -> LiveTVCoordinator {
LiveTVCoordinator()
}
func makeDownloads() -> DownloadListCoordinator {
DownloadListCoordinator()
}
#endif
@ViewBuilder

View File

@ -0,0 +1,37 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import JellyfinAPI
import Stinsen
import SwiftUI
final class MediaSourceInfoCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \MediaSourceInfoCoordinator.start)
@Root
var start = makeStart
@Route(.push)
var mediaStreamInfo = makeMediaStreamInfo
private let mediaSourceInfo: MediaSourceInfo
init(mediaSourceInfo: MediaSourceInfo) {
self.mediaSourceInfo = mediaSourceInfo
}
@ViewBuilder
func makeMediaStreamInfo(mediaStream: MediaStream) -> some View {
MediaStreamInfoView(mediaStream: mediaStream)
}
@ViewBuilder
func makeStart() -> some View {
ItemView.MediaSourceInfoView(mediaSource: mediaSourceInfo)
}
}

View File

@ -0,0 +1,46 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import JellyfinAPI
import Stinsen
import SwiftUI
final class PlaybackSettingsCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \PlaybackSettingsCoordinator.start)
@Root
var start = makeStart
@Route(.push)
var videoPlayerSettings = makeVideoPlayerSettings
#if os(iOS)
@Route(.push)
var mediaStreamInfo = makeMediaStreamInfo
#endif
func makeVideoPlayerSettings() -> VideoPlayerSettingsCoordinator {
VideoPlayerSettingsCoordinator()
}
#if os(iOS)
@ViewBuilder
func makeMediaStreamInfo(mediaStream: MediaStream) -> some View {
MediaStreamInfoView(mediaStream: mediaStream)
}
#endif
@ViewBuilder
func makeStart() -> some View {
#if os(iOS)
PlaybackSettingsView()
#else
EmptyView()
#endif
}
}

View File

@ -3,7 +3,7 @@
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Foundation

View File

@ -3,7 +3,7 @@
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Foundation
@ -17,11 +17,16 @@ final class SearchCoordinator: NavigationCoordinatable {
@Root
var start = makeStart
#if os(tvOS)
@Route(.modal)
var item = makeItem
@Route(.modal)
var library = makeLibrary
#else
@Route(.push)
var item = makeItem
@Route(.push)
var library = makeLibrary
#if !os(tvOS)
@Route(.modal)
var filter = makeFilter
#endif

View File

@ -3,7 +3,7 @@
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Foundation

View File

@ -3,10 +3,11 @@
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Foundation
import PulseUI
import Stinsen
import SwiftUI

View File

@ -3,49 +3,84 @@
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Foundation
import PulseUI
import Stinsen
import SwiftUI
final class SettingsCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \SettingsCoordinator.start)
@Root
var start = makeStart
#if os(iOS)
@Route(.push)
var serverDetail = makeServerDetail
var about = makeAbout
@Route(.push)
var overlaySettings = makeOverlaySettings
var appIconSelector = makeAppIconSelector
@Route(.push)
var experimentalSettings = makeExperimentalSettings
var log = makeLog
@Route(.push)
var nativePlayerSettings = makeNativePlayerSettings
@Route(.push)
var quickConnect = makeQuickConnectSettings
@Route(.push)
var customizeViewsSettings = makeCustomizeViewsSettings
@Route(.push)
var about = makeAbout
#if !os(tvOS)
var experimentalSettings = makeExperimentalSettings
@Route(.push)
var quickConnect = makeQuickConnectSettings
var indicatorSettings = makeIndicatorSettings
@Route(.push)
var fontPicker = makeFontPicker
var serverDetail = makeServerDetail
@Route(.push)
var videoPlayerSettings = makeVideoPlayerSettings
#endif
#if os(tvOS)
@Route(.modal)
var customizeViewsSettings = makeCustomizeViewsSettings
@Route(.modal)
var experimentalSettings = makeExperimentalSettings
@Route(.modal)
var indicatorSettings = makeIndicatorSettings
@Route(.modal)
var log = makeLog
@Route(.modal)
var serverDetail = makeServerDetail
@Route(.modal)
var videoPlayerSettings = makeVideoPlayerSettings
#endif
private let viewModel: SettingsViewModel
init() {
viewModel = .init()
}
#if os(iOS)
@ViewBuilder
func makeServerDetail() -> some View {
ServerDetailView(viewModel: .init(server: SessionManager.main.currentLogin.server))
func makeAbout() -> some View {
AboutAppView(viewModel: viewModel)
}
@ViewBuilder
func makeOverlaySettings() -> some View {
OverlaySettingsView()
func makeAppIconSelector() -> some View {
AppIconSelectorView(viewModel: viewModel)
}
@ViewBuilder
func makeExperimentalSettings() -> some View {
ExperimentalSettingsView()
func makeNativePlayerSettings() -> some View {
NativeVideoPlayerSettingsView()
}
@ViewBuilder
func makeQuickConnectSettings() -> some View {
QuickConnectSettingsView(viewModel: .init())
}
@ViewBuilder
@ -54,27 +89,70 @@ final class SettingsCoordinator: NavigationCoordinatable {
}
@ViewBuilder
func makeAbout() -> some View {
AboutAppView()
}
#if !os(tvOS)
@ViewBuilder
func makeQuickConnectSettings() -> some View {
let viewModel = QuickConnectSettingsViewModel()
QuickConnectSettingsView(viewModel: viewModel)
func makeExperimentalSettings() -> some View {
ExperimentalSettingsView()
}
@ViewBuilder
func makeFontPicker() -> some View {
FontPickerView()
.navigationTitle(L10n.subtitleFont)
func makeIndicatorSettings() -> some View {
IndicatorSettingsView()
}
@ViewBuilder
func makeServerDetail(server: ServerState) -> some View {
ServerDetailView(viewModel: .init(server: server))
}
func makeVideoPlayerSettings() -> VideoPlayerSettingsCoordinator {
VideoPlayerSettingsCoordinator()
}
#endif
#if os(tvOS)
func makeCustomizeViewsSettings() -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
NavigationViewCoordinator(
BasicNavigationViewCoordinator {
CustomizeViewsSettings()
}
)
}
func makeExperimentalSettings() -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
NavigationViewCoordinator(
BasicNavigationViewCoordinator {
ExperimentalSettingsView()
}
)
}
func makeIndicatorSettings() -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
NavigationViewCoordinator(
BasicNavigationViewCoordinator {
IndicatorSettingsView()
}
)
}
func makeServerDetail(server: ServerState) -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
NavigationViewCoordinator(
BasicNavigationViewCoordinator {
ServerDetailView(viewModel: .init(server: server))
}
)
}
func makeVideoPlayerSettings() -> NavigationViewCoordinator<VideoPlayerSettingsCoordinator> {
NavigationViewCoordinator(VideoPlayerSettingsCoordinator())
}
#endif
@ViewBuilder
func makeLog() -> some View {
ConsoleView()
}
@ViewBuilder
func makeStart() -> some View {
let viewModel = SettingsViewModel(server: SessionManager.main.currentLogin.server, user: SessionManager.main.currentLogin.user)
SettingsView(viewModel: viewModel)
}
}

View File

@ -3,7 +3,7 @@
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Foundation

View File

@ -3,7 +3,7 @@
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Foundation
@ -16,7 +16,7 @@ final class UserSignInCoordinator: NavigationCoordinatable {
@Root
var start = makeStart
#if !os(tvOS)
#if os(iOS)
@Route(.modal)
var quickConnect = makeQuickConnect
#endif
@ -27,7 +27,7 @@ final class UserSignInCoordinator: NavigationCoordinatable {
self.viewModel = viewModel
}
#if !os(tvOS)
#if os(iOS)
func makeQuickConnect() -> NavigationViewCoordinator<QuickConnectCoordinator> {
NavigationViewCoordinator(QuickConnectCoordinator(viewModel: viewModel))
}

View File

@ -0,0 +1,69 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Defaults
import Foundation
import JellyfinAPI
import Stinsen
import SwiftUI
final class VideoPlayerCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \VideoPlayerCoordinator.start)
@Root
var start = makeStart
let videoPlayerManager: VideoPlayerManager
init(manager: VideoPlayerManager) {
self.videoPlayerManager = manager
}
@ViewBuilder
func makeStart() -> some View {
#if os(iOS)
PreferenceUIHostingControllerView {
Group {
if Defaults[.VideoPlayer.videoPlayerType] == .swiftfin {
VideoPlayer(manager: self.videoPlayerManager)
.overlay {
VideoPlayer.Overlay()
}
} else {
NativeVideoPlayer(manager: self.videoPlayerManager)
}
}
.overrideViewPreference(.dark)
}
.ignoresSafeArea()
.hideSystemOverlays()
// .onAppear {
// AppDelegate.changeOrientation(.landscape)
// }
#else
PreferenceUIHostingControllerView {
Group {
if Defaults[.VideoPlayer.videoPlayerType] == .swiftfin {
VideoPlayer(manager: self.videoPlayerManager)
.overlay {
VideoPlayer.Overlay()
}
} else {
NativeVideoPlayer(manager: self.videoPlayerManager)
}
}
}
.ignoresSafeArea()
#endif
}
}

View File

@ -1,40 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
//
import Defaults
import Foundation
import JellyfinAPI
import Stinsen
import SwiftUI
final class LiveTVVideoPlayerCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \LiveTVVideoPlayerCoordinator.start)
@Root
var start = makeStart
let viewModel: VideoPlayerViewModel
init(viewModel: VideoPlayerViewModel) {
self.viewModel = viewModel
}
@ViewBuilder
func makeStart() -> some View {
if Defaults[.Experimental.liveTVNativePlayer] {
LiveTVNativePlayerView(viewModel: viewModel)
.navigationBarHidden(true)
.ignoresSafeArea()
} else {
LiveTVPlayerView(viewModel: viewModel)
.navigationBarHidden(true)
.ignoresSafeArea()
}
}
}

View File

@ -1,48 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
//
import Defaults
import Foundation
import JellyfinAPI
import Stinsen
import SwiftUI
final class VideoPlayerCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \VideoPlayerCoordinator.start)
@Root
var start = makeStart
let viewModel: VideoPlayerViewModel
init(viewModel: VideoPlayerViewModel) {
self.viewModel = viewModel
}
@ViewBuilder
func makeStart() -> some View {
PreferenceUIHostingControllerView {
if Defaults[.Experimental.nativePlayer] {
NativePlayerView(viewModel: self.viewModel)
.navigationBarHidden(true)
.statusBar(hidden: true)
.ignoresSafeArea()
.prefersHomeIndicatorAutoHidden(true)
.supportedOrientations(UIDevice.current.userInterfaceIdiom == .pad ? .all : .landscape)
} else {
VLCPlayerView(viewModel: self.viewModel)
.navigationBarHidden(true)
.statusBar(hidden: true)
.ignoresSafeArea()
.prefersHomeIndicatorAutoHidden(true)
.supportedOrientations(UIDevice.current.userInterfaceIdiom == .pad ? .all : .landscape)
}
}.ignoresSafeArea()
}
}

View File

@ -1,40 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
//
import Defaults
import Foundation
import JellyfinAPI
import Stinsen
import SwiftUI
final class LiveTVVideoPlayerCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \LiveTVVideoPlayerCoordinator.start)
@Root
var start = makeStart
let viewModel: VideoPlayerViewModel
init(viewModel: VideoPlayerViewModel) {
self.viewModel = viewModel
}
@ViewBuilder
func makeStart() -> some View {
if Defaults[.Experimental.liveTVNativePlayer] {
LiveTVNativeVideoPlayerView(viewModel: viewModel)
.navigationBarHidden(true)
.ignoresSafeArea()
} else {
LiveTVVideoPlayerView(viewModel: viewModel)
.navigationBarHidden(true)
.ignoresSafeArea()
}
}
}

View File

@ -1,40 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
//
import Defaults
import Foundation
import JellyfinAPI
import Stinsen
import SwiftUI
final class VideoPlayerCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \VideoPlayerCoordinator.start)
@Root
var start = makeStart
let viewModel: VideoPlayerViewModel
init(viewModel: VideoPlayerViewModel) {
self.viewModel = viewModel
}
@ViewBuilder
func makeStart() -> some View {
if Defaults[.Experimental.nativePlayer] {
NativePlayerView(viewModel: viewModel)
.navigationBarHidden(true)
.ignoresSafeArea()
} else {
VLCPlayerView(viewModel: viewModel)
.navigationBarHidden(true)
.ignoresSafeArea()
}
}
}

View File

@ -0,0 +1,59 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Defaults
import Stinsen
import SwiftUI
final class VideoPlayerSettingsCoordinator: NavigationCoordinatable {
let stack = NavigationStack(initial: \VideoPlayerSettingsCoordinator.start)
@Root
var start = makeStart
@Route(.push)
var fontPicker = makeFontPicker
#if os(iOS)
@Route(.push)
var gestureSettings = makeGestureSettings
@Route(.push)
var actionButtonSelector = makeActionButtonSelector
#endif
#if os(tvOS)
#endif
func makeFontPicker(selection: Binding<String>) -> some View {
FontPickerView(selection: selection)
.navigationTitle(L10n.subtitleFont)
}
#if os(iOS)
@ViewBuilder
func makeGestureSettings() -> some View {
GestureSettingsView()
.navigationTitle("Gestures")
}
func makeActionButtonSelector(selectedButtonsBinding: Binding<[VideoPlayerActionButton]>) -> some View {
ActionButtonSelectorView(selectedButtonsBinding: selectedButtonsBinding)
}
#endif
#if os(tvOS)
#endif
@ViewBuilder
func makeStart() -> some View {
VideoPlayerSettingsView()
}
}

View File

@ -3,29 +3,23 @@
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Foundation
import JellyfinAPI
struct ErrorMessage: Identifiable {
struct ErrorMessage: Hashable, Identifiable {
let code: Int
let title: String
let code: Int?
let message: String
// Chosen value such that if an error has this code, don't show the code to the UI
// This was chosen because of its unlikelyhood to ever be used
static let noShowErrorCode = -69420
var id: String {
"\(code)\(title)\(message)"
var id: Int {
hashValue
}
init(code: Int, title: String, message: String) {
init(message: String, code: Int? = nil) {
self.code = code
self.title = title
self.message = message
}
}

View File

@ -3,112 +3,112 @@
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Foundation
import JellyfinAPI
enum NetworkError: Error {
/// For the case that the ErrorResponse object has a code of -1
case URLError(response: ErrorResponse, displayMessage: String?)
/// For the case that the ErrorRespones object has a code of -2
case HTTPURLError(response: ErrorResponse, displayMessage: String?)
/// For the case that the ErrorResponse object has a positive code
case JellyfinError(response: ErrorResponse, displayMessage: String?)
var errorMessage: ErrorMessage {
switch self {
case let .URLError(response, displayMessage):
return NetworkError.parseURLError(from: response, displayMessage: displayMessage)
case let .HTTPURLError(response, displayMessage):
return NetworkError.parseHTTPURLError(from: response, displayMessage: displayMessage)
case let .JellyfinError(response, displayMessage):
return NetworkError.parseJellyfinError(from: response, displayMessage: displayMessage)
}
}
private static func parseURLError(from response: ErrorResponse, displayMessage: String?) -> ErrorMessage {
let errorMessage: ErrorMessage
switch response {
case let .error(_, _, _, err):
// Code references:
// https://developer.apple.com/documentation/foundation/1508628-url_loading_system_error_codes
switch err._code {
case -1001:
errorMessage = ErrorMessage(
code: err._code,
title: L10n.error,
message: L10n.networkTimedOut
)
case -1003:
errorMessage = ErrorMessage(
code: err._code,
title: L10n.error,
message: L10n.unableToFindHost
)
case -1004:
errorMessage = ErrorMessage(
code: err._code,
title: L10n.error,
message: L10n.cannotConnectToHost
)
default:
errorMessage = ErrorMessage(
code: err._code,
title: L10n.error,
message: L10n.unknownError
)
}
}
return errorMessage
}
private static func parseHTTPURLError(from response: ErrorResponse, displayMessage: String?) -> ErrorMessage {
let errorMessage: ErrorMessage
// Not implemented as has not run into one of these errors as time of writing
switch response {
case .error:
errorMessage = ErrorMessage(
code: 0,
title: L10n.error,
message: "An HTTP URL error has occurred"
)
}
return errorMessage
}
private static func parseJellyfinError(from response: ErrorResponse, displayMessage: String?) -> ErrorMessage {
let errorMessage: ErrorMessage
switch response {
case let .error(code, _, _, _):
// Generic HTTP status codes
switch code {
case 401:
errorMessage = ErrorMessage(
code: code,
title: L10n.unauthorized,
message: L10n.unauthorizedUser
)
default:
errorMessage = ErrorMessage(
code: code,
title: L10n.error,
message: displayMessage ?? L10n.unknownError
)
}
}
return errorMessage
}
}
// enum NetworkError: Error {
//
// /// For the case that the ErrorResponse object has a code of -1
// case URLError(response: ErrorResponse, displayMessage: String?)
//
// /// For the case that the ErrorRespones object has a code of -2
// case HTTPURLError(response: ErrorResponse, displayMessage: String?)
//
// /// For the case that the ErrorResponse object has a positive code
// case JellyfinError(response: ErrorResponse, displayMessage: String?)
//
// var errorMessage: ErrorMessage {
// switch self {
// case let .URLError(response, displayMessage):
// return NetworkError.parseURLError(from: response, displayMessage: displayMessage)
// case let .HTTPURLError(response, displayMessage):
// return NetworkError.parseHTTPURLError(from: response, displayMessage: displayMessage)
// case let .JellyfinError(response, displayMessage):
// return NetworkError.parseJellyfinError(from: response, displayMessage: displayMessage)
// }
// }
//
// private static func parseURLError(from response: ErrorResponse, displayMessage: String?) -> ErrorMessage {
// let errorMessage: ErrorMessage
//
// switch response {
// case let .error(_, _, _, err):
//
// // Code references:
// // https://developer.apple.com/documentation/foundation/1508628-url_loading_system_error_codes
// switch err._code {
// case -1001:
// errorMessage = ErrorMessage(
// code: err._code,
// title: L10n.error,
// message: L10n.networkTimedOut
// )
// case -1003:
// errorMessage = ErrorMessage(
// code: err._code,
// title: L10n.error,
// message: L10n.unableToFindHost
// )
// case -1004:
// errorMessage = ErrorMessage(
// code: err._code,
// title: L10n.error,
// message: L10n.cannotConnectToHost
// )
// default:
// errorMessage = ErrorMessage(
// code: err._code,
// title: L10n.error,
// message: L10n.unknownError
// )
// }
// }
//
// return errorMessage
// }
//
// private static func parseHTTPURLError(from response: ErrorResponse, displayMessage: String?) -> ErrorMessage {
// let errorMessage: ErrorMessage
//
// // Not implemented as has not run into one of these errors as time of writing
// switch response {
// case .error:
// errorMessage = ErrorMessage(
// code: 0,
// title: L10n.error,
// message: "An HTTP URL error has occurred"
// )
// }
//
// return errorMessage
// }
//
// private static func parseJellyfinError(from response: ErrorResponse, displayMessage: String?) -> ErrorMessage {
// let errorMessage: ErrorMessage
//
// switch response {
// case let .error(code, _, _, _):
//
// // Generic HTTP status codes
// switch code {
// case 401:
// errorMessage = ErrorMessage(
// code: code,
// title: L10n.unauthorized,
// message: L10n.unauthorizedUser
// )
// default:
// errorMessage = ErrorMessage(
// code: code,
// title: L10n.error,
// message: displayMessage ?? L10n.unknownError
// )
// }
// }
//
// return errorMessage
// }
// }

View File

@ -0,0 +1,48 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Foundation
extension Array {
func appending(_ element: Element) -> [Element] {
self + [element]
}
func appending(_ element: Element, if condition: Bool) -> [Element] {
if condition {
return self + [element]
} else {
return self
}
}
func appending(_ contents: [Element]) -> [Element] {
self + contents
}
func prepending(_ element: Element) -> [Element] {
[element] + self
}
func prepending(_ element: Element, if condition: Bool) -> [Element] {
if condition {
return [element] + self
} else {
return self
}
}
// There are instances where `removeFirst()` is called on an empty
// collection even with a count check and causes a crash
@discardableResult
mutating func removeFirstSafe() -> Element? {
guard count > 0 else { return nil }
return removeFirst()
}
}

View File

@ -1,33 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
//
import Foundation
extension Array {
func appending(_ element: Element) -> [Element] {
self + [element]
}
func appending(_ element: Element, if condition: Bool) -> [Element] {
if condition {
return self + [element]
} else {
return self
}
}
func appending(_ contents: [Element]) -> [Element] {
self + contents
}
}
extension ArraySlice {
var asArray: [Element] {
Array(self)
}
}

View File

@ -1,20 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
//
import Foundation
extension Bundle {
var iconFileName: String? {
guard let icons = infoDictionary?["CFBundleIcons"] as? [String: Any],
let primaryIcon = icons["CFBundlePrimaryIcon"] as? [String: Any],
let iconFiles = primaryIcon["CFBundleIconFiles"] as? [String],
let iconFileName = iconFiles.last
else { return nil }
return iconFileName
}
}

View File

@ -0,0 +1,29 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import SwiftUI
extension Button where Label: View {
/// Creates a Button with an empty action and a custom label.
init(role: ButtonRole? = nil, @ViewBuilder label: @escaping () -> Label) {
self.init {} label: {
label()
}
}
}
extension Button where Label == Text {
/// Creates a Button with an empty action and a plain text label.
init(_ title: String, role: ButtonRole? = nil) {
self.init(role: role) {
Text(title)
}
}
}

View File

@ -0,0 +1,19 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import UIKit
extension CGPoint {
func isNear(_ other: CGPoint, padding: CGFloat) -> Bool {
let xRange = (x - padding) ... (x + padding)
let yRange = (y - padding) ... (y + padding)
return xRange.contains(other.x) && yRange.contains(other.y)
}
}

View File

@ -0,0 +1,16 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import UIKit
extension CGSize {
static func Square(length: CGFloat) -> CGSize {
CGSize(width: length, height: length)
}
}

View File

@ -1,31 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
//
import UIKit
extension CGSize {
static func Circle(radius: CGFloat) -> CGSize {
CGSize(width: radius, height: radius)
}
// From https://gist.github.com/jkosoy/c835fea2c03e76720c77
static func aspectFill(aspectRatio: CGSize, minimumSize: CGSize) -> CGSize {
var minimumSize = minimumSize
let mW = minimumSize.width / aspectRatio.width
let mH = minimumSize.height / aspectRatio.height
if mH > mW {
minimumSize.width = minimumSize.height / aspectRatio.height * aspectRatio.width
} else if mW > mH {
minimumSize.height = minimumSize.width / aspectRatio.width * aspectRatio.height
}
return minimumSize
}
}

View File

@ -0,0 +1,24 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Foundation
extension Collection {
var asArray: [Element] {
Array(self)
}
func sorted<Value: Comparable>(using keyPath: KeyPath<Element, Value>) -> [Element] {
sorted(by: { $0[keyPath: keyPath] < $1[keyPath: keyPath] })
}
subscript(safe index: Index) -> Element? {
indices.contains(index) ? self[index] : nil
}
}

View File

@ -1,23 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
//
import Foundation
public extension Collection {
/// SwifterSwift: Safe protects the array from out of bounds by use of optional.
///
/// let arr = [1, 2, 3, 4, 5]
/// arr[safe: 1] -> 2
/// arr[safe: 10] -> nil
///
/// - Parameter index: index of element to access element.
subscript(safe index: Index) -> Element? {
indices.contains(index) ? self[index] : nil
}
}

View File

@ -3,15 +3,24 @@
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import SwiftUI
public extension Color {
extension Color {
internal static let jellyfinPurple = Color(uiColor: .jellyfinPurple)
static let jellyfinPurple = Color(uiColor: .jellyfinPurple)
var uiColor: UIColor {
UIColor(self)
}
var overlayColor: Color {
Color(uiColor: uiColor.overlayColor)
}
// TODO: Correct and add colors
#if os(tvOS) // tvOS doesn't have these
static let systemFill = Color(UIColor.white)
static let secondarySystemFill = Color(UIColor.gray)
@ -24,7 +33,3 @@ public extension Color {
static let tertiarySystemFill = Color(UIColor.tertiarySystemFill)
#endif
}
extension UIColor {
static let jellyfinPurple = UIColor(red: 172 / 255, green: 92 / 255, blue: 195 / 255, alpha: 1)
}

View File

@ -0,0 +1,27 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import CoreStore
import Foundation
import Logging
extension CoreStore.LogLevel {
var asSwiftLog: Logger.Level {
switch self {
case .trace:
return .trace
case .notice:
return .debug
case .warning:
return .warning
case .fatal:
return .critical
}
}
}

View File

@ -1,46 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
//
import Defaults
import Foundation
public extension Defaults.Serializable where Self: Codable {
static var bridge: Defaults.TopLevelCodableBridge<Self> { Defaults.TopLevelCodableBridge() }
}
public extension Defaults.Serializable where Self: Codable & NSSecureCoding {
static var bridge: Defaults.CodableNSSecureCodingBridge<Self> { Defaults.CodableNSSecureCodingBridge() }
}
public extension Defaults.Serializable where Self: Codable & NSSecureCoding & Defaults.PreferNSSecureCoding {
static var bridge: Defaults.NSSecureCodingBridge<Self> { Defaults.NSSecureCodingBridge() }
}
public extension Defaults.Serializable where Self: Codable & RawRepresentable {
static var bridge: Defaults.RawRepresentableCodableBridge<Self> { Defaults.RawRepresentableCodableBridge() }
}
public extension Defaults.Serializable where Self: Codable & RawRepresentable & Defaults.PreferRawRepresentable {
static var bridge: Defaults.RawRepresentableBridge<Self> { Defaults.RawRepresentableBridge() }
}
public extension Defaults.Serializable where Self: RawRepresentable {
static var bridge: Defaults.RawRepresentableBridge<Self> { Defaults.RawRepresentableBridge() }
}
public extension Defaults.Serializable where Self: NSSecureCoding {
static var bridge: Defaults.NSSecureCodingBridge<Self> { Defaults.NSSecureCodingBridge() }
}
public extension Defaults.CollectionSerializable where Element: Defaults.Serializable {
static var bridge: Defaults.CollectionBridge<Self> { Defaults.CollectionBridge() }
}
public extension Defaults.SetAlgebraSerializable where Element: Defaults.Serializable & Hashable {
static var bridge: Defaults.SetAlgebraBridge<Self> { Defaults.SetAlgebraBridge() }
}

View File

@ -1,22 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
//
import Foundation
extension Double {
func subtract(_ other: Double, floor: Double) -> Double {
var v = self - other
if v < floor {
v += abs(floor - v)
}
return v
}
}

View File

@ -3,18 +3,14 @@
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import SwiftUI
import UIKit
@main
struct JellyfinPlayer_tvOSApp: App {
extension UIEdgeInsets {
var body: some Scene {
WindowGroup {
MainCoordinator().view()
}
var asEdgeInsets: EdgeInsets {
EdgeInsets(top: top, leading: left, bottom: bottom, trailing: right)
}
}

View File

@ -0,0 +1,87 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import SwiftUI
// TODO: Look at name spacing
struct AudioOffset: EnvironmentKey {
static let defaultValue: Binding<Int> = .constant(0)
}
struct AspectFilled: EnvironmentKey {
static let defaultValue: Binding<Bool> = .constant(false)
}
struct CurrentOverlayType: EnvironmentKey {
static let defaultValue: Binding<VideoPlayer.OverlayType> = .constant(.main)
}
struct IsScrubbing: EnvironmentKey {
static let defaultValue: Binding<Bool> = .constant(false)
}
struct PlaybackSpeedKey: EnvironmentKey {
static let defaultValue: Binding<Float> = .constant(1)
}
struct SafeAreaInsetsKey: EnvironmentKey {
static var defaultValue: EdgeInsets {
UIApplication.shared.keyWindow?.safeAreaInsets.asEdgeInsets ?? .zero
}
}
struct SubtitleOffset: EnvironmentKey {
static let defaultValue: Binding<Int> = .constant(0)
}
struct IsPresentingOverlayKey: EnvironmentKey {
static let defaultValue: Binding<Bool> = .constant(false)
}
extension EnvironmentValues {
var isPresentingOverlay: Binding<Bool> {
get { self[IsPresentingOverlayKey.self] }
set { self[IsPresentingOverlayKey.self] = newValue }
}
var audioOffset: Binding<Int> {
get { self[AudioOffset.self] }
set { self[AudioOffset.self] = newValue }
}
var aspectFilled: Binding<Bool> {
get { self[AspectFilled.self] }
set { self[AspectFilled.self] = newValue }
}
var currentOverlayType: Binding<VideoPlayer.OverlayType> {
get { self[CurrentOverlayType.self] }
set { self[CurrentOverlayType.self] = newValue }
}
var isScrubbing: Binding<Bool> {
get { self[IsScrubbing.self] }
set { self[IsScrubbing.self] = newValue }
}
var playbackSpeed: Binding<Float> {
get { self[PlaybackSpeedKey.self] }
set { self[PlaybackSpeedKey.self] = newValue }
}
var safeAreaInsets: EdgeInsets {
self[SafeAreaInsetsKey.self]
}
var subtitleOffset: Binding<Int> {
get { self[SubtitleOffset.self] }
set { self[SubtitleOffset.self] = newValue }
}
}

View File

@ -0,0 +1,26 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Foundation
extension Equatable {
func random(in range: Range<Int>) -> [Self] {
Array(repeating: self, count: Int.random(in: range))
}
func repeating(count: Int) -> [Self] {
Array(repeating: self, count: count)
}
func mutating<Value>(_ keyPath: WritableKeyPath<Self, Value>, with newValue: Value) -> Self {
var copy = self
copy[keyPath: keyPath] = newValue
return copy
}
}

View File

@ -0,0 +1,34 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Foundation
#if os(iOS)
extension FileManager {
var availableStorage: Int {
let availableStorage: Int64
let fileURL = URL(fileURLWithPath: NSHomeDirectory() as String)
do {
let values = try fileURL.resourceValues(forKeys: [.volumeAvailableCapacityForImportantUsageKey])
if let capacity = values.volumeAvailableCapacityForImportantUsage {
availableStorage = capacity
} else {
availableStorage = -1
}
} catch {
availableStorage = -1
}
return Int(availableStorage)
}
}
#endif

View File

@ -3,14 +3,14 @@
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Foundation
import JellyfinAPI
extension RequestBuilder where T == URL {
var url: URL {
URL(string: URLString)!
extension Float {
var rateLabel: String {
String(format: "%.2f", self).appending("x")
}
}

View File

@ -3,15 +3,16 @@
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import SwiftUI
extension Font {
func toUIFont() -> UIFont {
var uiFont: UIFont {
switch self {
#if !os(tvOS)
#if os(iOS)
case .largeTitle:
return UIFont.preferredFont(forTextStyle: .largeTitle)
#endif

View File

@ -0,0 +1,20 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import SwiftUI
extension HorizontalAlignment {
struct VideoPlayerTitleAlignment: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat {
context[HorizontalAlignment.leading]
}
}
static let VideoPlayerTitleAlignmentGuide = HorizontalAlignment(VideoPlayerTitleAlignment.self)
}

View File

@ -0,0 +1,55 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Foundation
extension FixedWidthInteger {
var timeLabel: String {
let hours = self / 3600
let minutes = (self % 3600) / 60
let seconds = self % 3600 % 60
let hourText = hours > 0 ? String(hours).appending(":") : ""
let minutesText = hours > 0 ? String(minutes).leftPad(toWidth: 2, withString: "0").appending(":") : String(minutes)
.appending(":")
let secondsText = String(seconds).leftPad(toWidth: 2, withString: "0")
return hourText
.appending(minutesText)
.appending(secondsText)
}
}
extension Int {
/// Format if the current value represents milliseconds
var millisecondFormat: String {
let isNegative = self < 0
let value = abs(self)
let seconds = "\(value / 1000)"
let milliseconds = "\(value % 1000)".first ?? "0"
return seconds
.appending(".")
.appending(milliseconds)
.appending("s")
.prepending("-", if: isNegative)
}
// Format if the current value represents seconds
var secondFormat: String {
let isNegative = self < 0
let value = abs(self)
let seconds = "\(value)"
return seconds
.appending("s")
.prepending("-", if: isNegative)
}
}

View File

@ -3,15 +3,17 @@
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Foundation
import JellyfinAPI
extension APISortOrder {
typealias APISortOrder = JellyfinAPI.SortOrder
extension APISortOrder: Displayable {
// TODO: Localize
var localized: String {
var displayTitle: String {
switch self {
case .ascending:
return "Ascending"
@ -19,8 +21,11 @@ extension APISortOrder {
return "Descending"
}
}
}
extension APISortOrder {
var filter: ItemFilters.Filter {
.init(displayName: localized, filterName: rawValue)
.init(displayTitle: displayTitle, filterName: rawValue)
}
}

View File

@ -3,9 +3,10 @@
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Factory
import Foundation
import JellyfinAPI
import UIKit
@ -52,15 +53,15 @@ extension BaseItemDto {
// MARK: Series Images
func seriesImageURL(_ type: ImageType, maxWidth: Int? = nil, maxHeight: Int? = nil) -> URL {
_imageURL(type, maxWidth: maxWidth, maxHeight: maxHeight, itemID: seriesId ?? "")
_imageURL(type, maxWidth: maxWidth, maxHeight: maxHeight, itemID: seriesID ?? "")
}
func seriesImageURL(_ type: ImageType, maxWidth: CGFloat? = nil, maxHeight: CGFloat? = nil) -> URL {
_imageURL(type, maxWidth: Int(maxWidth), maxHeight: Int(maxHeight), itemID: seriesId ?? "")
_imageURL(type, maxWidth: Int(maxWidth), maxHeight: Int(maxHeight), itemID: seriesID ?? "")
}
func seriesImageSource(_ type: ImageType, maxWidth: Int? = nil, maxHeight: Int? = nil) -> ImageSource {
let url = _imageURL(type, maxWidth: maxWidth, maxHeight: maxHeight, itemID: seriesId ?? "")
let url = _imageURL(type, maxWidth: maxWidth, maxHeight: maxHeight, itemID: seriesID ?? "")
return ImageSource(url: url, blurHash: nil)
}
@ -80,16 +81,25 @@ extension BaseItemDto {
maxHeight: Int?,
itemID: String
) -> URL {
// TODO: See if the scaling is actually right so that it isn't so big
let scaleWidth = maxWidth == nil ? nil : UIScreen.main.scale(maxWidth!)
let scaleHeight = maxHeight == nil ? nil : UIScreen.main.scale(maxHeight!)
let tag = imageTags?[type.rawValue]
return ImageAPI.getItemImageWithRequestBuilder(
itemId: itemID,
imageType: type,
let client = Container.userSession.callAsFunction().client
let parameters = Paths.GetItemImageParameters(
maxWidth: scaleWidth,
maxHeight: scaleHeight,
tag: tag
).url
)
let request = Paths.getItemImage(
itemID: itemID,
imageType: type.rawValue,
parameters: parameters
)
return client.fullURL(with: request)
}
fileprivate func _imageSource(_ type: ImageType, maxWidth: Int?, maxHeight: Int?) -> ImageSource {

View File

@ -3,7 +3,7 @@
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Defaults
@ -18,9 +18,9 @@ extension BaseItemDto: Poster {
var title: String {
switch type {
case .episode:
return seriesName ?? displayName
return seriesName ?? displayTitle
default:
return displayName
return displayTitle
}
}
@ -28,6 +28,8 @@ extension BaseItemDto: Poster {
switch type {
case .episode:
return seasonEpisodeLocator
case .video:
return extraType?.displayTitle
default:
return nil
}
@ -63,6 +65,8 @@ extension BaseItemDto: Poster {
imageSource(.primary, maxWidth: maxWidth),
]
}
case .video:
return [imageSource(.primary, maxWidth: maxWidth)]
default:
return [
imageSource(.thumb, maxWidth: maxWidth),

View File

@ -0,0 +1,48 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Combine
import Defaults
import Factory
import Foundation
import JellyfinAPI
import SwiftUI
extension BaseItemDto {
func videoPlayerViewModel(with mediaSource: MediaSourceInfo) async throws -> VideoPlayerViewModel {
let builder = DeviceProfileBuilder()
// TODO: fix bitrate settings
let tempOverkillBitrate = 360_000_000
builder.setMaxBitrate(bitrate: tempOverkillBitrate)
let profile = builder.buildProfile()
let userSession = Container.userSession.callAsFunction()
let playbackInfo = PlaybackInfoDto(deviceProfile: profile)
let playbackInfoParameters = Paths.GetPostedPlaybackInfoParameters(
userID: userSession.user.id,
maxStreamingBitrate: tempOverkillBitrate
)
let request = Paths.getPostedPlaybackInfo(
itemID: self.id!,
parameters: playbackInfoParameters,
playbackInfo
)
let response = try await userSession.client.send(request)
guard let matchingMediaSource = response.value.mediaSources?
.first(where: { $0.eTag == mediaSource.eTag && $0.id == mediaSource.id })
else { throw JellyfinAPIError("Matching media source not in playback info") }
return try matchingMediaSource.videoPlayerViewModel(with: self, playSessionID: response.value.playSessionID!)
}
}

View File

@ -3,20 +3,22 @@
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Algorithms
import Factory
import Foundation
import JellyfinAPI
import UIKit
extension BaseItemDto: Displayable {
var displayName: String {
var displayTitle: String {
name ?? .emptyDash
}
}
extension BaseItemDto: Identifiable {}
extension BaseItemDto: LibraryParent {}
extension BaseItemDto {
@ -26,6 +28,11 @@ extension BaseItemDto {
return L10n.episodeNumber(episodeNo)
}
var runTimeSeconds: Int {
let playbackPositionTicks = runTimeTicks ?? 0
return Int(playbackPositionTicks / 10_000_000)
}
var seasonEpisodeLocator: String? {
if let seasonNo = parentIndexNumber, let episodeNo = indexNumber {
return L10n.seasonAndEpisode(String(seasonNo), String(episodeNo))
@ -33,8 +40,14 @@ extension BaseItemDto {
return nil
}
var startTimeSeconds: Int {
let playbackPositionTicks = userData?.playbackPositionTicks ?? 0
return Int(playbackPositionTicks / 10_000_000)
}
// MARK: Calculations
// TODO: make computed var or function that takes allowed units
func getItemRuntime() -> String? {
let timeHMSFormatter: DateComponentsFormatter = {
let formatter = DateComponentsFormatter()
@ -49,7 +62,7 @@ extension BaseItemDto {
return text
}
var progress: String? {
var progressLabel: String? {
guard let playbackPositionTicks = userData?.playbackPositionTicks,
let totalTicks = runTimeTicks,
playbackPositionTicks != 0,
@ -92,54 +105,6 @@ extension BaseItemDto {
return 0
}
// MARK: ItemDetail
struct ItemDetail {
let title: String
let content: String
}
func createInformationItems() -> [ItemDetail] {
var informationItems: [ItemDetail] = []
if let productionYear = productionYear {
informationItems.append(ItemDetail(title: L10n.released, content: "\(productionYear)"))
}
if let rating = officialRating {
informationItems.append(ItemDetail(title: L10n.rated, content: "\(rating)"))
}
if let runtime = getItemRuntime() {
informationItems.append(ItemDetail(title: L10n.runtime, content: runtime))
}
return informationItems
}
func createMediaItems() -> [ItemDetail] {
var mediaItems: [ItemDetail] = []
if let mediaStreams = mediaStreams {
let audioStreams = mediaStreams.filter { $0.type == .audio }
let subtitleStreams = mediaStreams.filter { $0.type == .subtitle }
if !audioStreams.isEmpty {
let audioList = audioStreams.compactMap { "\($0.displayTitle ?? L10n.noTitle) (\($0.codec ?? L10n.noCodec))" }
.joined(separator: "\n")
mediaItems.append(ItemDetail(title: L10n.audio, content: audioList))
}
if !subtitleStreams.isEmpty {
let subtitleList = subtitleStreams.compactMap { "\($0.displayTitle ?? L10n.noTitle) (\($0.codec ?? L10n.noCodec))" }
.joined(separator: "\n")
mediaItems.append(ItemDetail(title: L10n.subtitles, content: subtitleList))
}
}
return mediaItems
}
var subtitleStreams: [MediaStream] {
mediaStreams?.filter { $0.type == .subtitle } ?? []
}
@ -148,13 +113,17 @@ extension BaseItemDto {
mediaStreams?.filter { $0.type == .audio } ?? []
}
var videoStreams: [MediaStream] {
mediaStreams?.filter { $0.type == .video } ?? []
}
// MARK: Missing and Unaired
var missing: Bool {
var isMissing: Bool {
locationType == .virtual
}
var unaired: Bool {
var isUnaired: Bool {
if let premierDate = premiereDate {
return premierDate > Date()
} else {
@ -184,32 +153,86 @@ extension BaseItemDto {
// MARK: Chapter Images
func getChapterImage(maxWidth: Int) -> [URL] {
guard let chapters = chapters, !chapters.isEmpty else { return [] }
var fullChapterInfo: [ChapterInfo.FullInfo] {
guard let chapters else { return [] }
var chapterImageURLs: [URL] = []
let ranges: [Range<Int>] = []
.appending(chapters.map(\.startTimeSeconds))
.appending(runTimeSeconds + 1)
.adjacentPairs()
.map { $0 ..< $1 }
for chapterIndex in 0 ..< chapters.count {
let urlString = ImageAPI.getItemImageWithRequestBuilder(
itemId: id ?? "",
imageType: .chapter,
maxWidth: maxWidth,
imageIndex: chapterIndex
).URLString
chapterImageURLs.append(URL(string: urlString)!)
return chapters
.enumerated()
.map { index, chapterInfo in
let client = Container.userSession.callAsFunction().client
let parameters = Paths.GetItemImageParameters(
maxWidth: 500,
quality: 90,
imageIndex: index
)
let request = Paths.getItemImage(
itemID: id ?? "",
imageType: ImageType.chapter.rawValue,
parameters: parameters
)
let imageURL = client.fullURL(with: request)
let range = ranges.first(where: { $0.first == chapterInfo.startTimeSeconds }) ?? startTimeSeconds ..< startTimeSeconds + 1
return ChapterInfo.FullInfo(
chapterInfo: chapterInfo,
imageSource: .init(url: imageURL),
secondsRange: range
)
}
}
// TODO: series-season-episode hierarchy for episodes
// TODO: user hierarchy for downloads
var downloadFolder: URL? {
guard let type, let id else { return nil }
let root = URL.downloads
// .appendingPathComponent(userSession.user.id)
switch type {
case .movie, .episode:
return root
.appendingPathComponent(id)
// case .episode:
// guard let seasonID = seasonID,
// let seriesID = seriesID
// else {
// return nil
// }
// return root
// .appendingPathComponent(seriesID)
// .appendingPathComponent(seasonID)
// .appendingPathComponent(id)
default:
return nil
}
return chapterImageURLs
}
// TODO: Don't use spoof objects as a placeholder or no results
static var placeHolder: BaseItemDto {
.init(
name: "Placeholder",
id: "1",
overview: String(repeating: "a", count: 100),
indexNumber: 20
name: "Placeholder",
overview: String(repeating: "a", count: 100)
// indexNumber: 20
)
}
static func randomItem() -> BaseItemDto {
.init(
id: UUID().uuidString,
name: "Lorem Ipsum",
overview: "Lorem ipsum dolor sit amet"
)
}
@ -217,36 +240,3 @@ extension BaseItemDto {
.init(name: L10n.noResults)
}
}
extension BaseItemDtoImageBlurHashes {
subscript(imageType: ImageType) -> [String: String]? {
switch imageType {
case .primary:
return primary
case .art:
return art
case .backdrop:
return backdrop
case .banner:
return banner
case .logo:
return logo
case .thumb:
return thumb
case .disc:
return disc
case .box:
return box
case .screenshot:
return screenshot
case .menu:
return menu
case .chapter:
return chapter
case .boxRear:
return boxRear
case .profile:
return profile
}
}
}

View File

@ -3,9 +3,10 @@
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Factory
import Foundation
import JellyfinAPI
import UIKit
@ -22,12 +23,19 @@ extension BaseItemPerson: Poster {
func portraitPosterImageSource(maxWidth: CGFloat) -> ImageSource {
let scaleWidth = UIScreen.main.scale(maxWidth)
let url = ImageAPI.getItemImageWithRequestBuilder(
itemId: id ?? "",
imageType: .primary,
let client = Container.userSession.callAsFunction().client
let imageRequestParameters = Paths.GetItemImageParameters(
maxWidth: scaleWidth,
tag: primaryImageTag
).url
)
let imageRequest = Paths.getItemImage(
itemID: id ?? "",
imageType: ImageType.primary.rawValue,
parameters: imageRequestParameters
)
let url = client.fullURL(with: imageRequest)
var blurHash: String?

View File

@ -3,7 +3,7 @@
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
// Copyright (c) 2023 Jellyfin & Jellyfin Contributors
//
import Foundation
@ -11,7 +11,7 @@ import JellyfinAPI
import UIKit
extension BaseItemPerson: Displayable {
var displayName: String {
var displayTitle: String {
self.name ?? .emptyDash
}
}

Some files were not shown because too many files have changed in this diff Show More