New Year, New Video Player + Other Goodies (#593)
30
README.md
@ -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.
|
||||
|
16
Resources/AppIcons/Dark/AppIcon-dark-blue.svg
Normal 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 |
16
Resources/AppIcons/Dark/AppIcon-dark-green.svg
Normal 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 |
16
Resources/AppIcons/Dark/AppIcon-dark-jellyfin.svg
Normal 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 |
16
Resources/AppIcons/Dark/AppIcon-dark-orange.svg
Normal 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 |
16
Resources/AppIcons/Dark/AppIcon-dark-red.svg
Normal 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 |
16
Resources/AppIcons/Dark/AppIcon-dark-yellow.svg
Normal 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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
16
Resources/AppIcons/Light/AppIcon-light-blue.svg
Normal 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 |
16
Resources/AppIcons/Light/AppIcon-light-green.svg
Normal 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 |
16
Resources/AppIcons/Light/AppIcon-light-jellyfin.svg
Normal 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 |
16
Resources/AppIcons/Light/AppIcon-light-orange.svg
Normal 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 |
16
Resources/AppIcons/Light/AppIcon-light-red.svg
Normal 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 |
16
Resources/AppIcons/Light/AppIcon-light-yellow.svg
Normal 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 |
16
Resources/AppIcons/Primary/AppIcon-primary-primary.svg
Normal 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 |
46
Resources/Download_on_the_App_Store_Badge_US-UK_RGB_blk_092917.svg
Executable 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="<Group>">
|
||||
<g id="_Group_2" data-name="<Group>">
|
||||
<g id="_Group_3" data-name="<Group>">
|
||||
<path id="_Path_" data-name="<Path>" 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="<Path>" 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="<Group>">
|
||||
<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 |
16
Resources/primary-wide.svg
Normal 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 |
40
Shared/AppIcons/AppIcons.swift
Normal 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]))
|
||||
}
|
||||
}
|
38
Shared/AppIcons/DarkAppIcon.swift
Normal 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"
|
||||
}
|
38
Shared/AppIcons/InvertedDarkAppIcon.swift
Normal 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"
|
||||
}
|
38
Shared/AppIcons/InvertedLightAppIcon.swift
Normal 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"
|
||||
}
|
38
Shared/AppIcons/LightAppIcon.swift
Normal 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"
|
||||
}
|
23
Shared/AppIcons/PrimaryAppIcon.swift
Normal 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"
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
30
Shared/Coordinators/BasicNavigationCoordinator.swift
Normal 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()
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
|
32
Shared/Coordinators/DownloadListCoordinator.swift
Normal 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
|
32
Shared/Coordinators/DownloadTaskCoordinator.swift
Normal 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
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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))
|
||||
// }
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
37
Shared/Coordinators/MediaSourceInfoCoordinator.swift
Normal 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)
|
||||
}
|
||||
}
|
46
Shared/Coordinators/PlaybackSettingsCoordinator.swift
Normal 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
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
}
|
||||
|
69
Shared/Coordinators/VideoPlayerCoordinator.swift
Normal 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
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
59
Shared/Coordinators/VideoPlayerSettingsCoordinator.swift
Normal 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()
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
// }
|
||||
// }
|
||||
|
48
Shared/Extensions/Array.swift
Normal 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()
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
29
Shared/Extensions/Button.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
19
Shared/Extensions/CGPoint.swift
Normal 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)
|
||||
}
|
||||
}
|
16
Shared/Extensions/CGSize.swift
Normal 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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
24
Shared/Extensions/Collection.swift
Normal 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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
27
Shared/Extensions/CoreStore.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
@ -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() }
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
87
Shared/Extensions/EnvironmentValue.swift
Normal 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 }
|
||||
}
|
||||
}
|
26
Shared/Extensions/Equatable.swift
Normal 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
|
||||
}
|
||||
}
|
34
Shared/Extensions/Files.swift
Normal 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
|
@ -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")
|
||||
}
|
||||
}
|
@ -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
|
20
Shared/Extensions/HorizontalAlignment.swift
Normal 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)
|
||||
}
|
55
Shared/Extensions/Int.swift
Normal 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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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 {
|
@ -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),
|
@ -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!)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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?
|
||||
|
@ -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
|
||||
}
|
||||
}
|