mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-08-09 00:17:59 +00:00
feat(tracking): redesign tracking settings
This commit is contained in:
parent
b817eb5b14
commit
7fb8206107
14 changed files with 1581 additions and 908 deletions
|
|
@ -0,0 +1,15 @@
|
|||
package com.nuvio.app.features.simkl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import com.nuvio.app.R
|
||||
|
||||
@Composable
|
||||
actual fun simklBrandPainter(asset: SimklBrandAsset): Painter =
|
||||
painterResource(
|
||||
id = when (asset) {
|
||||
SimklBrandAsset.Glyph -> R.drawable.simkl_logo_glyph
|
||||
SimklBrandAsset.Wordmark -> R.drawable.simkl_logo_wordmark
|
||||
},
|
||||
)
|
||||
22
composeApp/src/androidMain/res/drawable/simkl_logo_glyph.xml
Normal file
22
composeApp/src/androidMain/res/drawable/simkl_logo_glyph.xml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="1536"
|
||||
android:viewportHeight="1536">
|
||||
<path android:pathData="M245.76,0H1290.24C1425.97,0 1536,110.03 1536,245.76V1290.24C1536,1425.97 1425.97,1536 1290.24,1536H245.76C110.03,1536 0,1425.97 0,1290.24V245.76C0,110.03 110.03,0 245.76,0Z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:startX="768"
|
||||
android:startY="0"
|
||||
android:endX="768"
|
||||
android:endY="1536"
|
||||
android:startColor="#FF000000"
|
||||
android:endColor="#FF333333"
|
||||
android:type="linear" />
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M205.832,970.284c0,35.322 1.82,65.9 5.433,91.774 6.709,53.176 28.198,94.359 64.507,123.51 33.579,27.302 88.907,45.656 165.984,55.065 63.178,7.983 165.983,11.993 308.45,11.993 234.287,0 377.455,-7.982 429.428,-23.947 52.859,-15.964 90.936,-45.77 114.232,-89.46 23.295,-44.153 34.944,-113.446 34.944,-207.843 0,-84.525 -11.907,-145.798 -35.645,-183.858 -15.702,-25.836 -36.64,-45.772 -62.885,-59.885 -26.244,-14.074 -61.52,-24.64 -105.9,-31.697 -43.937,-7.018 -145.452,-12.917 -304.58,-17.622 -175.752,-5.63 -274.576,-11.49 -296.545,-17.584 -26.024,-7.519 -39,-28.188 -39,-62.006 0,-35.205 12.312,-57.03 36.972,-65.514 21.97,-7.018 92.115,-10.566 210.476,-10.566 119.65,0 194.955,1.427 225.92,4.203 34.942,3.317 55.584,18.04 61.85,44.26 2.25,8.908 3.835,24.833 4.72,47.777h271c0.368,-22.442 0.589,-38.83 0.589,-49.127 0,-95.475 -21.527,-161.676 -64.58,-198.657 -31.847,-26.683 -83.858,-45.386 -156.032,-56.183 -54.7,-7.944 -148.4,-11.915 -281.099,-11.915 -204.024,0 -333.149,4.704 -387.407,14.075 -60.082,10.797 -104.942,33.047 -134.505,66.748 -39.477,44.538 -59.198,121.158 -59.198,229.823 0,51.555 4.017,93.278 12.09,125.129 14.338,57.649 44.822,98.407 91.451,122.276 36.309,18.741 98.861,29.538 187.622,32.353 34.058,0.926 108.444,4.204 223.228,9.833 111.172,5.63 184.23,8.445 219.212,8.445 50.645,0.926 82.678,8.213 96.168,21.787 9.843,9.832 14.783,26.915 14.783,51.286 0,35.63 -7.852,58.612 -23.518,68.908 -11.206,7.018 -34.502,11.953 -69.887,14.73 -18.837,0.963 -87.84,1.658 -207.011,2.12 -86.954,-0.462 -138.265,-0.924 -153.93,-1.387 -31.369,-0.926 -53.338,-2.584 -65.871,-4.936 -12.532,-2.314 -23.738,-6.44 -33.58,-12.996 -18.835,-13.573 -28.014,-44.036 -27.571,-91.389H205.832v50.477Z" />
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="122dp"
|
||||
android:height="20dp"
|
||||
android:viewportWidth="989"
|
||||
android:viewportHeight="162.133">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M0,115.012a105.623,105.623 0,0 0,0.935 15.345c1.15,8.9 4.959,15.778 11.356,20.556 5.894,4.632 15.669,7.672 29.253,9.192q16.6,2.063 54.337,2.027c41.257,0 66.413,-1.3 75.613,-3.909 9.344,-2.606 16.028,-7.455 20.125,-14.548s6.181,-18.385 6.181,-33.73c0,-13.68 -2.012,-23.668 -6.037,-29.893a27.048,27.048 0,0 0,-10.638 -9.7c-4.456,-2.244 -10.35,-3.981 -17.825,-5.139s-24.581,-2.1 -51.462,-2.9q-44.527,-1.3 -50.025,-2.823c-4.457,-1.158 -6.613,-4.56 -6.613,-10.061q0,-8.577 6.253,-10.64c3.666,-1.158 15.454,-1.737 35.435,-1.737 20.125,0 32.775,0.217 38.022,0.579 5.893,0.507 9.343,2.678 10.421,6.442a34.224,34.224 0,0 1,0.791 6.876h47.941a56.136,56.136 0,0 1,-0.863 -7.962c0,-15.344 -3.737,-26.057 -11.212,-31.992 -5.607,-4.27 -14.663,-7.31 -27.241,-9.047Q140.517,-0.007 105.8,-0.007q-53.259,0 -67.49,2.317C27.816,4.047 20.053,7.666 14.878,13.167 7.978,20.405 4.6,32.926 4.6,50.587c0,8.4 0.647,15.2 2.013,20.412 2.443,9.409 7.547,16.068 15.453,19.9 6.109,3.113 16.675,4.85 31.7,5.284 5.75,0.145 18.256,0.724 37.662,1.592 18.76,0.941 31.122,1.376 37.016,1.376 8.553,0.217 13.944,1.375 16.244,3.546 1.653,1.665 2.515,4.488 2.515,8.4 0,5.791 -1.365,9.555 -3.953,11.219 -1.941,1.158 -5.894,1.955 -11.787,2.389 -3.235,0.145 -14.879,0.289 -34.932,0.362 -14.734,-0.073 -23.431,-0.145 -26.018,-0.217a77.288,77.288 0,0 1,-11.141 -0.8,14.991 14.991,0 0,1 -5.606,-2.243c-3.235,-2.244 -4.816,-7.311 -4.672,-15.273H1.15L0,115.012ZM216.2,162.133h50.6V0h-50.6v162.133ZM289.8,162.133h48.875l-1.222,-120.441h8.7l61.381,120.441h41.113l61.378,-120.441h8.122l-1.222,120.441H565.8V0h-89.412l-48.372,105.676L379.644,0H289.8v162.133ZM588.8,162.133h50.6V97.28h27.169l72.741,64.853h75.18L712.641,76.579 800.71,0h-73.2l-60.938,55.589H639.4V0h-50.6v162.133ZM818.8,162.133H989v-41.691H869.4V0h-50.6v162.133Z" />
|
||||
<path
|
||||
android:fillAlpha="0.302"
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M928.39,8.912V2.204H887.2v6.708h17.16v40.245h6.87V8.912ZM986.73,49.157 976.44,2.204H973l-15.44,33.538 -15.44,-33.538h-3.44l-10.29,46.953h6.86l5.15,-30.184 15.44,30.184h3.44l15.44,-30.184 5.15,30.184Z" />
|
||||
</vector>
|
||||
|
|
@ -0,0 +1 @@
|
|||
<svg style="background-color:#ffffff00" xmlns="http://www.w3.org/2000/svg" width="1536" height="1536"><defs><linearGradient id="a" x1="49.89%" y1="-.045%" x2="49.89%" y2="99.858%"><stop offset="0%"/><stop stop-color="#333" offset="100%"/></linearGradient></defs><rect width="1536" height="1536" fill="url(#a)" rx="245.76" ry="245.76"/><path d="M205.832 970.284c0 35.322 1.82 65.9 5.433 91.774 6.709 53.176 28.198 94.359 64.507 123.51 33.579 27.302 88.907 45.656 165.984 55.065 63.178 7.983 165.983 11.993 308.45 11.993 234.287 0 377.455-7.982 429.428-23.947 52.859-15.964 90.936-45.77 114.232-89.46 23.295-44.153 34.944-113.446 34.944-207.843 0-84.525-11.907-145.798-35.645-183.858-15.702-25.836-36.64-45.772-62.885-59.885-26.244-14.074-61.52-24.64-105.9-31.697-43.937-7.018-145.452-12.917-304.58-17.622-175.752-5.63-274.576-11.49-296.545-17.584-26.024-7.519-39-28.188-39-62.006 0-35.205 12.312-57.03 36.972-65.514 21.97-7.018 92.115-10.566 210.476-10.566 119.65 0 194.955 1.427 225.92 4.203 34.942 3.317 55.584 18.04 61.85 44.26 2.25 8.908 3.835 24.833 4.72 47.777h271c.368-22.442.589-38.83.589-49.127 0-95.475-21.527-161.676-64.58-198.657-31.847-26.683-83.858-45.386-156.032-56.183-54.7-7.944-148.4-11.915-281.099-11.915-204.024 0-333.149 4.704-387.407 14.075-60.082 10.797-104.942 33.047-134.505 66.748-39.477 44.538-59.198 121.158-59.198 229.823 0 51.555 4.017 93.278 12.09 125.129 14.338 57.649 44.822 98.407 91.451 122.276 36.309 18.741 98.861 29.538 187.622 32.353 34.058.926 108.444 4.204 223.228 9.833 111.172 5.63 184.23 8.445 219.212 8.445 50.645.926 82.678 8.213 96.168 21.787 9.843 9.832 14.783 26.915 14.783 51.286 0 35.63-7.852 58.612-23.518 68.908-11.206 7.018-34.502 11.953-69.887 14.73-18.837.963-87.84 1.658-207.011 2.12-86.954-.462-138.265-.924-153.93-1.387-31.369-.926-53.338-2.584-65.871-4.936-12.532-2.314-23.738-6.44-33.58-12.996-18.835-13.573-28.014-44.036-27.571-91.389H205.832v50.824-.347z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="989" height="162.133"><g fill="#fff"><path d="M0 115.012a105.623 105.623 0 0 0 .935 15.345c1.15 8.9 4.959 15.778 11.356 20.556 5.894 4.632 15.669 7.672 29.253 9.192q16.6 2.063 54.337 2.027c41.257 0 66.413-1.3 75.613-3.909 9.344-2.606 16.028-7.455 20.125-14.548s6.181-18.385 6.181-33.73c0-13.68-2.012-23.668-6.037-29.893a27.048 27.048 0 0 0-10.638-9.7c-4.456-2.244-10.35-3.981-17.825-5.139s-24.581-2.1-51.462-2.9q-44.527-1.3-50.025-2.823c-4.457-1.158-6.613-4.56-6.613-10.061q0-8.577 6.253-10.64c3.666-1.158 15.454-1.737 35.435-1.737 20.125 0 32.775.217 38.022.579 5.893.507 9.343 2.678 10.421 6.442a34.224 34.224 0 0 1 .791 6.876h47.941a56.136 56.136 0 0 1-.863-7.962c0-15.344-3.737-26.057-11.212-31.992-5.607-4.27-14.663-7.31-27.241-9.047Q140.517-.007 105.8-.007q-53.259 0-67.49 2.317C27.816 4.047 20.053 7.666 14.878 13.167 7.978 20.405 4.6 32.926 4.6 50.587c0 8.4.647 15.2 2.013 20.412 2.443 9.409 7.547 16.068 15.453 19.9 6.109 3.113 16.675 4.85 31.7 5.284 5.75.145 18.256.724 37.662 1.592 18.76.941 31.122 1.376 37.016 1.376 8.553.217 13.944 1.375 16.244 3.546 1.653 1.665 2.515 4.488 2.515 8.4 0 5.791-1.365 9.555-3.953 11.219-1.941 1.158-5.894 1.955-11.787 2.389-3.235.145-14.879.289-34.932.362-14.734-.073-23.431-.145-26.018-.217a77.288 77.288 0 0 1-11.141-.8 14.991 14.991 0 0 1-5.606-2.243c-3.235-2.244-4.816-7.311-4.672-15.273H1.15L0 115.012ZM216.2 162.133h50.6V0h-50.6v162.133ZM289.8 162.133h48.875l-1.222-120.441h8.7l61.381 120.441h41.113l61.378-120.441h8.122l-1.222 120.441H565.8V0h-89.412l-48.372 105.676L379.644 0H289.8v162.133ZM588.8 162.133h50.6V97.28h27.169l72.741 64.853h75.18L712.641 76.579 800.71 0h-73.2l-60.938 55.589H639.4V0h-50.6v162.133ZM818.8 162.133H989v-41.691H869.4V0h-50.6v162.133Z"/><g opacity=".302"><path d="M928.39 8.912V2.204H887.2v6.708h17.16v40.245h6.87V8.912ZM986.73 49.157 976.44 2.204H973l-15.44 33.538-15.44-33.538h-3.44l-10.29 46.953h6.86l5.15-30.184 15.44 30.184h3.44l15.44-30.184 5.15 30.184Z"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 2 KiB |
|
|
@ -1,4 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 104.55 45">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="104.55" height="45" viewBox="0 0 104.55 45">
|
||||
<g transform="translate(-65.25 0)">
|
||||
<path fill="#FFFFFF" d="M65.25,19.5199v-5.73h18.18v5.73H65.25zM76.93,44.7099c-2.42,0 -4.3,-0.62 -5.64,-1.86c-1.34,-1.24 -2.01,-3.01 -2.01,-5.32l-0.03,-15.44c0,-0.6 0.13,-1.14 0.41,-1.63c0.27,-0.49 0.65,-0.8 1.13,-0.93l-1.54,-4.52v-0.81l1.51,-9.1299h5V35.8999c0,1.08 0.24,1.83 0.71,2.26c0.48,0.43 1.36,0.64 2.67,0.64c0.84,0 1.61,0 2.32,-0.01c0.71,-0.01 1.35,-0.02 1.93,-0.04v5.61c-0.99,0.19 -2.05,0.29 -3.2,0.32c-1.15,0.03 -2.23,0.04 -3.25,0.04zM86,44.2599v-30.47h5.64l0.73,11.73l-0.59,-1.62c0.13,-1.23 0.38,-2.48 0.75,-3.74c0.37,-1.27 0.89,-2.43 1.56,-3.49c0.67,-1.06 1.54,-1.91 2.62,-2.56c1.08,-0.64 2.38,-0.96 3.91,-0.96c0.39,0 0.77,0.02 1.13,0.06c0.36,0.04 0.7,0.1 1.02,0.2v7.04c-0.63,-0.22 -1.3,-0.36 -2.01,-0.42c-0.71,-0.06 -1.37,-0.08 -1.98,-0.08c-0.73,0 -1.51,0.14 -2.35,0.43c-0.84,0.29 -1.61,0.72 -2.32,1.3c-0.71,0.58 -1.22,1.29 -1.54,2.15v20.44H86zM112.7,44.6c-1.82,0 -3.46,-0.35 -4.9,-1.05c-1.44,-0.7 -2.57,-1.68 -3.38,-2.93c-0.81,-1.26 -1.21,-2.7 -1.21,-4.34c0,-1.79 0.33,-3.26 0.98,-4.43c0.65,-1.16 1.59,-2.1 2.82,-2.82c1.23,-0.72 2.7,-1.25 4.41,-1.61c1.17,-0.26 2.42,-0.4701 3.74,-0.6301c1.32,-0.16 2.56,-0.2899 3.7,-0.3899c1.14,-0.1001 2.02,-0.17 2.64,-0.21v-1.26c0,-2.08 -0.41,-3.64 -1.24,-4.68c-0.83,-1.03 -2.23,-1.55 -4.2,-1.55c-1.27,0 -2.33,0.16 -3.18,0.47c-0.86,0.32 -1.5,0.9 -1.94,1.75c-0.44,0.85 -0.67,2.07 -0.71,3.67h-6c-0.21,-2.62 0.17,-4.78 1.13,-6.46c0.96,-1.68 2.39,-2.9301 4.3,-3.73c1.91,-0.8 4.17,-1.2 6.77,-1.2c1.53,0 2.98,0.17 4.36,0.52c1.38,0.35 2.6,0.93 3.66,1.76c1.06,0.83 1.9,1.96 2.51,3.39c0.61,1.43 0.92,3.25 0.92,5.45v19.94h-6.45l0.08,-6.8101c-0.35,2.05 -1.25,3.75 -2.69,5.11c-1.44,1.3601 -3.48,2.0401 -6.1,2.0401zM114.49,39.6c0.97,0 1.96,-0.1601 2.96,-0.4901c1,-0.33 1.88,-0.8 2.64,-1.4399c0.75,-0.63 1.22,-1.38 1.41,-2.23v-5.59c-0.74,0.08 -1.62,0.2 -2.64,0.38c-1.02,0.18 -1.89,0.35 -2.61,0.52c-2.08,0.41 -3.59,0.96 -4.52,1.66c-0.93,0.7 -1.4,1.77 -1.4,3.23c0,0.82 0.2,1.53 0.59,2.12c0.39,0.6 0.9,1.06 1.54,1.38c0.63,0.33 1.31,0.48 2.04,0.46zM129.44,44.2599v-30.48h6.67v30.48H129.44zM135.4,36.6499l-0.67,-5.81l3.71,-6.46l7.53,-10.61h7.8l-10.42,14.23l-2.66,2.01l-5.28,6.62zM146.84,44.2599l-8.89,-15.43l5.03,-3.86l11.8,19.28H146.84z"/>
|
||||
<path fill="#FFFFFF" d="M151.62,19.5199v-5.73h18.18v5.73H151.62zM163.29,44.7099c-2.42,0 -4.3,-0.62 -5.64,-1.86c-1.34,-1.24 -2.01,-3.01 -2.01,-5.32l-0.03,-15.44c0,-0.6 0.13,-1.14 0.41,-1.63c0.27,-0.49 0.65,-0.8 1.13,-0.93l-1.54,-4.52v-0.81l1.51,-9.1299h5V35.8999c0,1.08 0.24,1.83 0.71,2.26c0.48,0.43 1.36,0.64 2.67,0.64c0.84,0 1.61,0 2.32,-0.01c0.71,-0.01 1.35,-0.02 1.93,-0.04v5.61c-0.99,0.19 -2.05,0.29 -3.2,0.32c-1.15,0.03 -2.23,0.04 -3.25,0.04z"/>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
|
@ -1121,10 +1121,24 @@
|
|||
<string name="settings_trakt_open_login">Open Trakt Login</string>
|
||||
<string name="settings_trakt_save_actions_description">Your Save actions can now target Trakt watchlist and personal lists.</string>
|
||||
<string name="settings_trakt_sign_in_description">Sign in with Trakt to enable list-based saving and Trakt library mode.</string>
|
||||
<string name="settings_tracking_intro_description">Connect one or more tracking services. Playback events are sent to every connected tracker, while you choose one source for library and watch progress views.</string>
|
||||
<string name="settings_tracking_data_sources">DATA SOURCES</string>
|
||||
<string name="settings_tracking_features">SOURCE PREFERENCES</string>
|
||||
<string name="settings_tracking_viewing_discovery">VIEWING AND DISCOVERY</string>
|
||||
<string name="settings_tracking_services">TRACKING SERVICES</string>
|
||||
<string name="settings_tracking_approval_redirect">After approval, you will be redirected back automatically.</string>
|
||||
<string name="settings_tracking_connect_first">Connect %1$s first</string>
|
||||
<string name="settings_tracking_disconnect_title">Disconnect %1$s?</string>
|
||||
<string name="settings_tracking_disconnect_description">This removes the connection from Nuvio. Your data on %1$s stays intact, and unavailable library or progress sources temporarily fall back to Nuvio.</string>
|
||||
<string name="settings_tracking_source_fallback">%1$s is unavailable. Using %2$s until it reconnects.</string>
|
||||
<string name="settings_tracking_progress_refresh_failed">The source changed, but the latest watch progress could not be refreshed.</string>
|
||||
<string name="settings_tracking_nuvio_library_description">Keep saved titles in your Nuvio library.</string>
|
||||
<string name="settings_tracking_trakt_library_description">Use your Trakt watchlist and personal lists.</string>
|
||||
<string name="settings_tracking_simkl_library_description">Use your Simkl plan-to-watch and status lists.</string>
|
||||
<string name="settings_tracking_nuvio_progress_description">Resume with progress stored by Nuvio Sync.</string>
|
||||
<string name="settings_tracking_trakt_progress_description">Resume with playback progress from Trakt.</string>
|
||||
<string name="settings_tracking_simkl_progress_description">Resume with playback progress from Simkl.</string>
|
||||
<string name="settings_tracking_tmdb_recommendations_description">Use TMDB recommendations on details pages.</string>
|
||||
<string name="settings_tracking_trakt_recommendations_description">Use personalized related titles from Trakt.</string>
|
||||
<string name="settings_simkl_connect">Connect Simkl</string>
|
||||
<string name="settings_simkl_connected_as">Connected as %1$s</string>
|
||||
<string name="settings_simkl_default_user">Simkl user</string>
|
||||
|
|
|
|||
|
|
@ -230,11 +230,12 @@ internal fun SettingsSection(
|
|||
@Composable
|
||||
internal fun SettingsNavigationRow(
|
||||
title: String,
|
||||
description: String,
|
||||
description: String?,
|
||||
icon: ImageVector? = null,
|
||||
iconPainter: Painter? = null,
|
||||
enabled: Boolean = true,
|
||||
isTablet: Boolean,
|
||||
trailingContent: (@Composable RowScope.() -> Unit)? = null,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val tokens = MaterialTheme.nuvio
|
||||
|
|
@ -294,15 +295,18 @@ internal fun SettingsNavigationRow(
|
|||
color = tokens.colors.textPrimary,
|
||||
fontWeight = FontWeight.Medium,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Text(
|
||||
text = description,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = tokens.colors.textMuted,
|
||||
modifier = Modifier.alpha(0.92f),
|
||||
)
|
||||
if (!description.isNullOrBlank()) {
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Text(
|
||||
text = description,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = tokens.colors.textMuted,
|
||||
modifier = Modifier.alpha(0.92f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
trailingContent?.invoke(this)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,255 @@
|
|||
package com.nuvio.app.features.settings
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Check
|
||||
import androidx.compose.material3.BasicAlertDialog
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.nuvio.app.core.ui.NuvioBottomSheetDivider
|
||||
import com.nuvio.app.core.ui.NuvioModalBottomSheet
|
||||
import com.nuvio.app.core.ui.dismissNuvioBottomSheet
|
||||
import com.nuvio.app.core.ui.nuvio
|
||||
import kotlinx.coroutines.launch
|
||||
import nuvio.composeapp.generated.resources.Res
|
||||
import nuvio.composeapp.generated.resources.action_close
|
||||
import nuvio.composeapp.generated.resources.cd_selected
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
internal data class TrackingPickerOption<T>(
|
||||
val value: T,
|
||||
val title: String,
|
||||
val description: String? = null,
|
||||
val enabled: Boolean = true,
|
||||
val unavailableReason: String? = null,
|
||||
)
|
||||
|
||||
@Composable
|
||||
internal fun <T> TrackingAdaptivePicker(
|
||||
isTablet: Boolean,
|
||||
title: String,
|
||||
subtitle: String,
|
||||
selectedValue: T,
|
||||
options: List<TrackingPickerOption<T>>,
|
||||
onSelected: (T) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
if (isTablet) {
|
||||
TrackingPickerDialog(
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
selectedValue = selectedValue,
|
||||
options = options,
|
||||
onSelected = onSelected,
|
||||
onDismiss = onDismiss,
|
||||
)
|
||||
} else {
|
||||
TrackingPickerBottomSheet(
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
selectedValue = selectedValue,
|
||||
options = options,
|
||||
onSelected = onSelected,
|
||||
onDismiss = onDismiss,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun <T> TrackingPickerBottomSheet(
|
||||
title: String,
|
||||
subtitle: String,
|
||||
selectedValue: T,
|
||||
options: List<TrackingPickerOption<T>>,
|
||||
onSelected: (T) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
fun dismiss() {
|
||||
scope.launch {
|
||||
dismissNuvioBottomSheet(sheetState = sheetState, onDismiss = onDismiss)
|
||||
}
|
||||
}
|
||||
|
||||
NuvioModalBottomSheet(
|
||||
onDismissRequest = ::dismiss,
|
||||
sheetState = sheetState,
|
||||
) {
|
||||
TrackingPickerContent(
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
selectedValue = selectedValue,
|
||||
options = options,
|
||||
isTablet = false,
|
||||
onSelected = { value ->
|
||||
onSelected(value)
|
||||
dismiss()
|
||||
},
|
||||
onDismiss = ::dismiss,
|
||||
modifier = Modifier.navigationBarsPadding(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun <T> TrackingPickerDialog(
|
||||
title: String,
|
||||
subtitle: String,
|
||||
selectedValue: T,
|
||||
options: List<TrackingPickerOption<T>>,
|
||||
onSelected: (T) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
val tokens = MaterialTheme.nuvio
|
||||
BasicAlertDialog(onDismissRequest = onDismiss) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.widthIn(max = tokens.components.dialogMaxWidth),
|
||||
shape = tokens.shapes.dialog,
|
||||
color = tokens.colors.surfaceDialog,
|
||||
) {
|
||||
TrackingPickerContent(
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
selectedValue = selectedValue,
|
||||
options = options,
|
||||
isTablet = true,
|
||||
onSelected = { value ->
|
||||
onSelected(value)
|
||||
onDismiss()
|
||||
},
|
||||
onDismiss = onDismiss,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun <T> TrackingPickerContent(
|
||||
title: String,
|
||||
subtitle: String,
|
||||
selectedValue: T,
|
||||
options: List<TrackingPickerOption<T>>,
|
||||
isTablet: Boolean,
|
||||
onSelected: (T) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val tokens = MaterialTheme.nuvio
|
||||
val horizontalPadding = if (isTablet) tokens.spacing.dialogPadding else tokens.spacing.screenHorizontal
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
start = horizontalPadding,
|
||||
end = horizontalPadding,
|
||||
top = if (isTablet) tokens.spacing.dialogPadding else 12.dp,
|
||||
bottom = 12.dp,
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = tokens.colors.textPrimary,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
Text(
|
||||
text = subtitle,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = tokens.colors.textMuted,
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = 520.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
options.forEachIndexed { index, option ->
|
||||
if (index > 0) {
|
||||
NuvioBottomSheetDivider()
|
||||
}
|
||||
TrackingPickerOptionRow(
|
||||
option = option,
|
||||
selected = option.value == selectedValue,
|
||||
isTablet = isTablet,
|
||||
onClick = { onSelected(option.value) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (isTablet) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = horizontalPadding, vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
) {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringResource(Res.string.action_close))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun <T> TrackingPickerOptionRow(
|
||||
option: TrackingPickerOption<T>,
|
||||
selected: Boolean,
|
||||
isTablet: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val tokens = MaterialTheme.nuvio
|
||||
val description = listOfNotNull(
|
||||
option.description?.takeIf(String::isNotBlank),
|
||||
option.unavailableReason?.takeIf(String::isNotBlank),
|
||||
).joinToString("\n").takeIf(String::isNotBlank)
|
||||
SettingsNavigationRow(
|
||||
title = option.title,
|
||||
description = description,
|
||||
enabled = option.enabled,
|
||||
isTablet = isTablet,
|
||||
trailingContent = {
|
||||
if (selected) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Check,
|
||||
contentDescription = stringResource(Res.string.cd_selected),
|
||||
tint = tokens.colors.accent,
|
||||
modifier = Modifier.size(24.dp),
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,765 @@
|
|||
package com.nuvio.app.features.settings
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Sync
|
||||
import androidx.compose.material3.BasicAlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.nuvio.app.core.ui.NuvioLoadingIndicator
|
||||
import com.nuvio.app.core.ui.NuvioTokens
|
||||
import com.nuvio.app.core.ui.nuvio
|
||||
import com.nuvio.app.features.simkl.SimklAuthError
|
||||
import com.nuvio.app.features.simkl.SimklAuthRepository
|
||||
import com.nuvio.app.features.simkl.SimklAuthUiState
|
||||
import com.nuvio.app.features.simkl.SimklBrandAsset
|
||||
import com.nuvio.app.features.simkl.SimklConnectionMode
|
||||
import com.nuvio.app.features.simkl.SimklSyncRepository
|
||||
import com.nuvio.app.features.simkl.simklBrandPainter
|
||||
import com.nuvio.app.features.tracking.TrackingRefreshIntent
|
||||
import com.nuvio.app.features.trakt.TraktAuthRepository
|
||||
import com.nuvio.app.features.trakt.TraktAuthUiState
|
||||
import com.nuvio.app.features.trakt.TraktBrandAsset
|
||||
import com.nuvio.app.features.trakt.TraktConnectionMode
|
||||
import com.nuvio.app.features.trakt.traktBrandPainter
|
||||
import kotlinx.coroutines.launch
|
||||
import nuvio.composeapp.generated.resources.Res
|
||||
import nuvio.composeapp.generated.resources.action_cancel
|
||||
import nuvio.composeapp.generated.resources.settings_simkl_authorization_expired
|
||||
import nuvio.composeapp.generated.resources.settings_simkl_authorization_revoked
|
||||
import nuvio.composeapp.generated.resources.settings_simkl_connect
|
||||
import nuvio.composeapp.generated.resources.settings_simkl_connected_as
|
||||
import nuvio.composeapp.generated.resources.settings_simkl_connected_description
|
||||
import nuvio.composeapp.generated.resources.settings_simkl_default_user
|
||||
import nuvio.composeapp.generated.resources.settings_simkl_disconnect
|
||||
import nuvio.composeapp.generated.resources.settings_simkl_finish_sign_in
|
||||
import nuvio.composeapp.generated.resources.settings_simkl_invalid_callback
|
||||
import nuvio.composeapp.generated.resources.settings_simkl_missing_credentials
|
||||
import nuvio.composeapp.generated.resources.settings_simkl_open_login
|
||||
import nuvio.composeapp.generated.resources.settings_simkl_sign_in_description
|
||||
import nuvio.composeapp.generated.resources.settings_simkl_sign_in_failed
|
||||
import nuvio.composeapp.generated.resources.settings_simkl_sync_info_action
|
||||
import nuvio.composeapp.generated.resources.settings_simkl_sync_now
|
||||
import nuvio.composeapp.generated.resources.settings_simkl_visit
|
||||
import nuvio.composeapp.generated.resources.settings_tracking_approval_redirect
|
||||
import nuvio.composeapp.generated.resources.settings_tracking_disconnect_description
|
||||
import nuvio.composeapp.generated.resources.settings_tracking_disconnect_title
|
||||
import nuvio.composeapp.generated.resources.settings_trakt_approval_redirect
|
||||
import nuvio.composeapp.generated.resources.settings_trakt_connect
|
||||
import nuvio.composeapp.generated.resources.settings_trakt_connected_as
|
||||
import nuvio.composeapp.generated.resources.settings_trakt_default_user
|
||||
import nuvio.composeapp.generated.resources.settings_trakt_disconnect
|
||||
import nuvio.composeapp.generated.resources.settings_trakt_failed_open_browser
|
||||
import nuvio.composeapp.generated.resources.settings_trakt_finish_sign_in
|
||||
import nuvio.composeapp.generated.resources.settings_trakt_missing_credentials
|
||||
import nuvio.composeapp.generated.resources.settings_trakt_open_login
|
||||
import nuvio.composeapp.generated.resources.settings_trakt_save_actions_description
|
||||
import nuvio.composeapp.generated.resources.settings_trakt_sign_in_description
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
internal enum class TrackingBrand(val displayName: String) {
|
||||
NUVIO("Nuvio"),
|
||||
TRAKT("Trakt"),
|
||||
SIMKL("Simkl"),
|
||||
TMDB("TMDB"),
|
||||
}
|
||||
|
||||
internal enum class TrackingConnectionCardMode {
|
||||
DISCONNECTED,
|
||||
AWAITING_APPROVAL,
|
||||
CONNECTED,
|
||||
}
|
||||
|
||||
internal fun isTrackingBrandAvailable(
|
||||
brand: TrackingBrand,
|
||||
traktConnected: Boolean,
|
||||
simklConnected: Boolean,
|
||||
): Boolean = when (brand) {
|
||||
TrackingBrand.NUVIO,
|
||||
TrackingBrand.TMDB,
|
||||
-> true
|
||||
TrackingBrand.TRAKT -> traktConnected
|
||||
TrackingBrand.SIMKL -> simklConnected
|
||||
}
|
||||
|
||||
internal fun TraktConnectionMode.toTrackingConnectionCardMode(): TrackingConnectionCardMode = when (this) {
|
||||
TraktConnectionMode.DISCONNECTED -> TrackingConnectionCardMode.DISCONNECTED
|
||||
TraktConnectionMode.AWAITING_APPROVAL -> TrackingConnectionCardMode.AWAITING_APPROVAL
|
||||
TraktConnectionMode.CONNECTED -> TrackingConnectionCardMode.CONNECTED
|
||||
}
|
||||
|
||||
internal fun SimklConnectionMode.toTrackingConnectionCardMode(): TrackingConnectionCardMode = when (this) {
|
||||
SimklConnectionMode.DISCONNECTED -> TrackingConnectionCardMode.DISCONNECTED
|
||||
SimklConnectionMode.AWAITING_APPROVAL -> TrackingConnectionCardMode.AWAITING_APPROVAL
|
||||
SimklConnectionMode.CONNECTED -> TrackingConnectionCardMode.CONNECTED
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun TrackingProviderCards(
|
||||
isTablet: Boolean,
|
||||
traktUiState: TraktAuthUiState,
|
||||
simklUiState: SimklAuthUiState,
|
||||
) {
|
||||
val syncState by remember {
|
||||
SimklSyncRepository.ensureLoaded()
|
||||
SimklSyncRepository.state
|
||||
}.collectAsStateWithLifecycle()
|
||||
val scope = rememberCoroutineScope()
|
||||
var showSyncInfo by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
BoxWithConstraints(modifier = Modifier.fillMaxWidth()) {
|
||||
val useTwoColumns = maxWidth >= 600.dp
|
||||
if (useTwoColumns) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(IntrinsicSize.Min),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
TraktProviderCard(
|
||||
uiState = traktUiState,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight(),
|
||||
)
|
||||
SimklProviderCard(
|
||||
uiState = simklUiState,
|
||||
isSyncing = syncState.isLoading,
|
||||
syncErrorMessage = syncState.errorMessage,
|
||||
onSyncRequested = {
|
||||
scope.launch {
|
||||
SimklSyncRepository.refresh(TrackingRefreshIntent.USER_INITIATED)
|
||||
}
|
||||
},
|
||||
onInfoRequested = { showSyncInfo = true },
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight(),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(if (isTablet) 16.dp else 12.dp),
|
||||
) {
|
||||
TraktProviderCard(
|
||||
uiState = traktUiState,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
SimklProviderCard(
|
||||
uiState = simklUiState,
|
||||
isSyncing = syncState.isLoading,
|
||||
syncErrorMessage = syncState.errorMessage,
|
||||
onSyncRequested = {
|
||||
scope.launch {
|
||||
SimklSyncRepository.refresh(TrackingRefreshIntent.USER_INITIATED)
|
||||
}
|
||||
},
|
||||
onInfoRequested = { showSyncInfo = true },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showSyncInfo) {
|
||||
SimklSyncInfoDialog(onDismiss = { showSyncInfo = false })
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TraktProviderCard(
|
||||
uiState: TraktAuthUiState,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
TrackingProviderCard(
|
||||
brand = TrackingBrand.TRAKT,
|
||||
mode = uiState.mode.toTrackingConnectionCardMode(),
|
||||
credentialsConfigured = uiState.credentialsConfigured,
|
||||
isLoading = uiState.isLoading,
|
||||
connectedLabel = stringResource(
|
||||
Res.string.settings_trakt_connected_as,
|
||||
uiState.username ?: stringResource(Res.string.settings_trakt_default_user),
|
||||
),
|
||||
connectedDescription = stringResource(Res.string.settings_trakt_save_actions_description),
|
||||
signInDescription = stringResource(Res.string.settings_trakt_sign_in_description),
|
||||
finishSignInLabel = stringResource(Res.string.settings_trakt_finish_sign_in),
|
||||
approvalDescription = stringResource(Res.string.settings_trakt_approval_redirect),
|
||||
connectLabel = stringResource(Res.string.settings_trakt_connect),
|
||||
openLoginLabel = stringResource(Res.string.settings_trakt_open_login),
|
||||
disconnectLabel = stringResource(Res.string.settings_trakt_disconnect),
|
||||
missingCredentialsMessage = stringResource(Res.string.settings_trakt_missing_credentials),
|
||||
statusMessage = uiState.statusMessage.takeUnless {
|
||||
uiState.mode == TraktConnectionMode.CONNECTED
|
||||
},
|
||||
errorMessage = uiState.errorMessage,
|
||||
onConnectRequested = TraktAuthRepository::onConnectRequested,
|
||||
onResumeAuthorization = {
|
||||
TraktAuthRepository.pendingAuthorizationUrl()
|
||||
?: TraktAuthRepository.onConnectRequested()
|
||||
},
|
||||
onCancelAuthorization = TraktAuthRepository::onCancelAuthorization,
|
||||
onDisconnect = TraktAuthRepository::onDisconnectRequested,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SimklProviderCard(
|
||||
uiState: SimklAuthUiState,
|
||||
isSyncing: Boolean,
|
||||
syncErrorMessage: String?,
|
||||
onSyncRequested: () -> Unit,
|
||||
onInfoRequested: () -> Unit,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
TrackingProviderCard(
|
||||
brand = TrackingBrand.SIMKL,
|
||||
mode = uiState.mode.toTrackingConnectionCardMode(),
|
||||
credentialsConfigured = uiState.credentialsConfigured,
|
||||
isLoading = uiState.isLoading,
|
||||
connectedLabel = stringResource(
|
||||
Res.string.settings_simkl_connected_as,
|
||||
uiState.username ?: stringResource(Res.string.settings_simkl_default_user),
|
||||
),
|
||||
connectedDescription = stringResource(Res.string.settings_simkl_connected_description),
|
||||
signInDescription = stringResource(Res.string.settings_simkl_sign_in_description),
|
||||
finishSignInLabel = stringResource(Res.string.settings_simkl_finish_sign_in),
|
||||
approvalDescription = stringResource(Res.string.settings_tracking_approval_redirect),
|
||||
connectLabel = stringResource(Res.string.settings_simkl_connect),
|
||||
openLoginLabel = stringResource(Res.string.settings_simkl_open_login),
|
||||
disconnectLabel = stringResource(Res.string.settings_simkl_disconnect),
|
||||
syncLabel = stringResource(Res.string.settings_simkl_sync_now),
|
||||
infoLabel = stringResource(Res.string.settings_simkl_sync_info_action),
|
||||
isSyncing = isSyncing,
|
||||
missingCredentialsMessage = stringResource(Res.string.settings_simkl_missing_credentials),
|
||||
errorMessage = simklErrorMessage(uiState.error) ?: syncErrorMessage,
|
||||
websiteLabel = stringResource(Res.string.settings_simkl_visit),
|
||||
websiteUrl = SIMKL_WEBSITE_URL,
|
||||
onConnectRequested = SimklAuthRepository::onConnectRequested,
|
||||
onResumeAuthorization = {
|
||||
SimklAuthRepository.pendingAuthorizationUrl()
|
||||
?: SimklAuthRepository.onConnectRequested()
|
||||
},
|
||||
onCancelAuthorization = SimklAuthRepository::onCancelAuthorization,
|
||||
onSyncRequested = onSyncRequested,
|
||||
onInfoRequested = onInfoRequested,
|
||||
onDisconnect = SimklAuthRepository::onDisconnectRequested,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TrackingProviderCard(
|
||||
brand: TrackingBrand,
|
||||
mode: TrackingConnectionCardMode,
|
||||
credentialsConfigured: Boolean,
|
||||
isLoading: Boolean,
|
||||
connectedLabel: String,
|
||||
connectedDescription: String,
|
||||
signInDescription: String,
|
||||
finishSignInLabel: String,
|
||||
approvalDescription: String,
|
||||
connectLabel: String,
|
||||
openLoginLabel: String,
|
||||
disconnectLabel: String,
|
||||
missingCredentialsMessage: String,
|
||||
modifier: Modifier = Modifier,
|
||||
syncLabel: String? = null,
|
||||
infoLabel: String? = null,
|
||||
isSyncing: Boolean = false,
|
||||
statusMessage: String? = null,
|
||||
errorMessage: String? = null,
|
||||
websiteLabel: String? = null,
|
||||
websiteUrl: String? = null,
|
||||
onConnectRequested: () -> String?,
|
||||
onResumeAuthorization: () -> String?,
|
||||
onCancelAuthorization: () -> Unit,
|
||||
onSyncRequested: (() -> Unit)? = null,
|
||||
onInfoRequested: (() -> Unit)? = null,
|
||||
onDisconnect: () -> Unit,
|
||||
) {
|
||||
val tokens = MaterialTheme.nuvio
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val failedOpenBrowserMessage = stringResource(Res.string.settings_trakt_failed_open_browser)
|
||||
var browserError by rememberSaveable { mutableStateOf(false) }
|
||||
var showDisconnectDialog by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
fun openUrl(url: String?) {
|
||||
if (url.isNullOrBlank()) return
|
||||
browserError = false
|
||||
runCatching { uriHandler.openUri(url) }
|
||||
.onFailure { browserError = true }
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clip(tokens.shapes.card)
|
||||
.background(brand.cardBrush(), tokens.shapes.card)
|
||||
.border(
|
||||
width = tokens.borders.hairline,
|
||||
color = Color.White.copy(alpha = 0.2f),
|
||||
shape = tokens.shapes.card,
|
||||
),
|
||||
) {
|
||||
TrackingBrandGlyph(
|
||||
brand = brand,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(12.dp)
|
||||
.size(150.dp)
|
||||
.alpha(0.08f),
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(if (mode == TrackingConnectionCardMode.CONNECTED) 20.dp else 18.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(14.dp),
|
||||
) {
|
||||
TrackingBrandWordmark(
|
||||
brand = brand,
|
||||
contentDescription = brand.displayName,
|
||||
)
|
||||
|
||||
when (mode) {
|
||||
TrackingConnectionCardMode.CONNECTED -> {
|
||||
TrackingConnectedIdentity(
|
||||
label = connectedLabel,
|
||||
description = connectedDescription,
|
||||
)
|
||||
if (syncLabel != null && onSyncRequested != null) {
|
||||
TrackingBrandPrimaryButton(
|
||||
label = syncLabel,
|
||||
loading = isSyncing,
|
||||
enabled = !isLoading && !isSyncing,
|
||||
onClick = onSyncRequested,
|
||||
showSyncIcon = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
TrackingConnectionCardMode.AWAITING_APPROVAL -> {
|
||||
Text(
|
||||
text = finishSignInLabel,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
Text(
|
||||
text = approvalDescription,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = Color.White.copy(alpha = 0.78f),
|
||||
)
|
||||
TrackingBrandPrimaryButton(
|
||||
label = openLoginLabel,
|
||||
loading = isLoading,
|
||||
enabled = !isLoading,
|
||||
onClick = { openUrl(onResumeAuthorization()) },
|
||||
)
|
||||
OutlinedButton(
|
||||
onClick = onCancelAuthorization,
|
||||
enabled = !isLoading,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 48.dp),
|
||||
border = BorderStroke(1.dp, Color.White.copy(alpha = 0.44f)),
|
||||
colors = ButtonDefaults.outlinedButtonColors(
|
||||
contentColor = Color.White,
|
||||
disabledContentColor = Color.White.copy(alpha = 0.45f),
|
||||
),
|
||||
) {
|
||||
Text(stringResource(Res.string.action_cancel))
|
||||
}
|
||||
}
|
||||
|
||||
TrackingConnectionCardMode.DISCONNECTED -> {
|
||||
Text(
|
||||
text = signInDescription,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = Color.White.copy(alpha = 0.82f),
|
||||
)
|
||||
TrackingBrandPrimaryButton(
|
||||
label = connectLabel,
|
||||
loading = isLoading,
|
||||
enabled = credentialsConfigured && !isLoading,
|
||||
onClick = { openUrl(onConnectRequested()) },
|
||||
)
|
||||
if (!credentialsConfigured) {
|
||||
TrackingBrandMessage(
|
||||
text = missingCredentialsMessage,
|
||||
isError = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
statusMessage?.takeIf(String::isNotBlank)?.let { message ->
|
||||
TrackingBrandMessage(text = message, isError = false)
|
||||
}
|
||||
errorMessage?.takeIf(String::isNotBlank)?.let { message ->
|
||||
TrackingBrandMessage(text = message, isError = true)
|
||||
}
|
||||
if (browserError) {
|
||||
TrackingBrandMessage(text = failedOpenBrowserMessage, isError = true)
|
||||
}
|
||||
|
||||
val hasWebsiteAction = !websiteLabel.isNullOrBlank() && !websiteUrl.isNullOrBlank()
|
||||
val hasDisconnectAction = mode == TrackingConnectionCardMode.CONNECTED
|
||||
val hasInfoAction = mode == TrackingConnectionCardMode.CONNECTED &&
|
||||
infoLabel != null && onInfoRequested != null
|
||||
val footerActionCount = listOf(
|
||||
hasWebsiteAction,
|
||||
hasDisconnectAction,
|
||||
hasInfoAction,
|
||||
).count { it }
|
||||
if (footerActionCount > 0) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(2.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (hasWebsiteAction) {
|
||||
TextButton(
|
||||
onClick = { openUrl(websiteUrl) },
|
||||
modifier = if (footerActionCount > 1) Modifier.weight(0.95f) else Modifier,
|
||||
contentPadding = PaddingValues(horizontal = 2.dp, vertical = 0.dp),
|
||||
colors = ButtonDefaults.textButtonColors(contentColor = Color.White),
|
||||
) {
|
||||
Text(
|
||||
text = websiteLabel.orEmpty(),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (hasDisconnectAction) {
|
||||
TextButton(
|
||||
onClick = { showDisconnectDialog = true },
|
||||
modifier = if (footerActionCount > 1) Modifier.weight(1.05f) else Modifier,
|
||||
enabled = !isLoading && !isSyncing,
|
||||
contentPadding = PaddingValues(horizontal = 2.dp, vertical = 0.dp),
|
||||
colors = ButtonDefaults.textButtonColors(
|
||||
contentColor = Color.White.copy(alpha = 0.84f),
|
||||
disabledContentColor = Color.White.copy(alpha = 0.38f),
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
text = disconnectLabel,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (hasInfoAction) {
|
||||
TextButton(
|
||||
onClick = { onInfoRequested?.invoke() },
|
||||
modifier = if (footerActionCount > 1) Modifier.weight(1.55f) else Modifier,
|
||||
contentPadding = PaddingValues(horizontal = 2.dp, vertical = 0.dp),
|
||||
colors = ButtonDefaults.textButtonColors(contentColor = Color.White),
|
||||
) {
|
||||
Text(
|
||||
text = infoLabel.orEmpty(),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showDisconnectDialog) {
|
||||
TrackingDisconnectDialog(
|
||||
brand = brand,
|
||||
onConfirm = {
|
||||
showDisconnectDialog = false
|
||||
onDisconnect()
|
||||
},
|
||||
onDismiss = { showDisconnectDialog = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TrackingConnectedIdentity(
|
||||
label: String,
|
||||
description: String,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
Text(
|
||||
text = description,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color.White.copy(alpha = 0.76f),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TrackingBrandPrimaryButton(
|
||||
label: String,
|
||||
loading: Boolean,
|
||||
enabled: Boolean,
|
||||
onClick: () -> Unit,
|
||||
showSyncIcon: Boolean = false,
|
||||
) {
|
||||
Button(
|
||||
onClick = onClick,
|
||||
enabled = enabled,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 48.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.White,
|
||||
contentColor = Color(0xFF171717),
|
||||
disabledContainerColor = Color.White.copy(alpha = 0.34f),
|
||||
disabledContentColor = Color.White.copy(alpha = 0.7f),
|
||||
),
|
||||
) {
|
||||
when {
|
||||
loading -> NuvioLoadingIndicator(
|
||||
color = Color(0xFF171717),
|
||||
modifier = Modifier.size(18.dp),
|
||||
)
|
||||
showSyncIcon -> Icon(
|
||||
imageVector = Icons.Rounded.Sync,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp),
|
||||
)
|
||||
}
|
||||
if (loading || showSyncIcon) {
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
}
|
||||
Text(label)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TrackingBrandMessage(
|
||||
text: String,
|
||||
isError: Boolean,
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = if (isError) TrackingErrorColor.copy(alpha = 0.14f) else Color.White.copy(alpha = 0.1f),
|
||||
shape = RoundedCornerShape(NuvioTokens.Radius.md),
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = if (isError) TrackingErrorColor else Color.White.copy(alpha = 0.82f),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TrackingDisconnectDialog(
|
||||
brand: TrackingBrand,
|
||||
onConfirm: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
val tokens = MaterialTheme.nuvio
|
||||
BasicAlertDialog(onDismissRequest = onDismiss) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.widthIn(max = tokens.components.dialogMaxWidth),
|
||||
shape = tokens.shapes.dialog,
|
||||
color = tokens.colors.surfaceDialog,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(tokens.spacing.dialogPadding),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(Res.string.settings_tracking_disconnect_title, brand.displayName),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = tokens.colors.textPrimary,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(Res.string.settings_tracking_disconnect_description, brand.displayName),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = tokens.colors.textMuted,
|
||||
)
|
||||
FlowRow(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringResource(Res.string.action_cancel))
|
||||
}
|
||||
Button(
|
||||
onClick = onConfirm,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.error,
|
||||
contentColor = MaterialTheme.colorScheme.onError,
|
||||
),
|
||||
) {
|
||||
Text(stringResource(Res.string.settings_trakt_disconnect))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun TrackingBrandGlyph(
|
||||
brand: TrackingBrand,
|
||||
modifier: Modifier = Modifier,
|
||||
contentDescription: String? = null,
|
||||
) {
|
||||
when (brand) {
|
||||
TrackingBrand.TRAKT -> Image(
|
||||
painter = traktBrandPainter(TraktBrandAsset.Glyph),
|
||||
contentDescription = contentDescription,
|
||||
modifier = modifier,
|
||||
contentScale = ContentScale.Fit,
|
||||
)
|
||||
TrackingBrand.SIMKL -> Image(
|
||||
painter = simklBrandPainter(SimklBrandAsset.Glyph),
|
||||
contentDescription = contentDescription,
|
||||
modifier = modifier,
|
||||
contentScale = ContentScale.Fit,
|
||||
)
|
||||
TrackingBrand.TMDB -> Image(
|
||||
painter = integrationLogoPainter(IntegrationLogo.Tmdb),
|
||||
contentDescription = contentDescription,
|
||||
modifier = modifier,
|
||||
contentScale = ContentScale.Fit,
|
||||
)
|
||||
TrackingBrand.NUVIO -> Icon(
|
||||
imageVector = Icons.Rounded.Sync,
|
||||
contentDescription = contentDescription,
|
||||
modifier = modifier,
|
||||
tint = MaterialTheme.nuvio.colors.accent,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TrackingBrandWordmark(
|
||||
brand: TrackingBrand,
|
||||
contentDescription: String,
|
||||
) {
|
||||
val painter: Painter = when (brand) {
|
||||
TrackingBrand.TRAKT -> traktBrandPainter(TraktBrandAsset.Wordmark)
|
||||
TrackingBrand.SIMKL -> simklBrandPainter(SimklBrandAsset.Wordmark)
|
||||
TrackingBrand.NUVIO,
|
||||
TrackingBrand.TMDB,
|
||||
-> return
|
||||
}
|
||||
Image(
|
||||
painter = painter,
|
||||
contentDescription = contentDescription,
|
||||
modifier = when (brand) {
|
||||
TrackingBrand.TRAKT -> Modifier
|
||||
.width(86.dp)
|
||||
.height(38.dp)
|
||||
TrackingBrand.SIMKL -> Modifier
|
||||
.width(124.dp)
|
||||
.height(30.dp)
|
||||
TrackingBrand.NUVIO,
|
||||
TrackingBrand.TMDB,
|
||||
-> Modifier
|
||||
},
|
||||
contentScale = ContentScale.Fit,
|
||||
alignment = Alignment.CenterStart,
|
||||
)
|
||||
}
|
||||
|
||||
private fun TrackingBrand.cardBrush(): Brush = when (this) {
|
||||
TrackingBrand.TRAKT -> Brush.linearGradient(
|
||||
colors = listOf(Color(0xFF7D279B), Color(0xFFD61F56), Color(0xFFF22125)),
|
||||
)
|
||||
TrackingBrand.SIMKL -> Brush.linearGradient(
|
||||
colors = listOf(Color(0xFF050505), Color(0xFF292929), Color(0xFF111111)),
|
||||
)
|
||||
TrackingBrand.NUVIO,
|
||||
TrackingBrand.TMDB,
|
||||
-> Brush.linearGradient(colors = listOf(Color(0xFF242424), Color(0xFF111111)))
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun simklErrorMessage(error: SimklAuthError?): String? = when (error) {
|
||||
null, SimklAuthError.MISSING_CLIENT_ID -> null
|
||||
SimklAuthError.INVALID_CALLBACK,
|
||||
SimklAuthError.INVALID_CALLBACK_STATE,
|
||||
-> stringResource(Res.string.settings_simkl_invalid_callback)
|
||||
SimklAuthError.AUTHORIZATION_EXPIRED ->
|
||||
stringResource(Res.string.settings_simkl_authorization_expired)
|
||||
SimklAuthError.TOKEN_EXCHANGE_FAILED,
|
||||
SimklAuthError.INVALID_TOKEN_RESPONSE,
|
||||
-> stringResource(Res.string.settings_simkl_sign_in_failed)
|
||||
SimklAuthError.AUTHORIZATION_REVOKED ->
|
||||
stringResource(Res.string.settings_simkl_authorization_revoked)
|
||||
}
|
||||
|
||||
private val TrackingErrorColor = Color(0xFFFFDAD6)
|
||||
private const val SIMKL_WEBSITE_URL = "https://simkl.com"
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,12 @@
|
|||
package com.nuvio.app.features.simkl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
|
||||
enum class SimklBrandAsset {
|
||||
Glyph,
|
||||
Wordmark,
|
||||
}
|
||||
|
||||
@Composable
|
||||
expect fun simklBrandPainter(asset: SimklBrandAsset): Painter
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
package com.nuvio.app.features.settings
|
||||
|
||||
import com.nuvio.app.features.simkl.SimklConnectionMode
|
||||
import com.nuvio.app.features.trakt.MoreLikeThisSourcePreference
|
||||
import com.nuvio.app.features.trakt.TraktConnectionMode
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class TrackingSettingsPresentationTest {
|
||||
@Test
|
||||
fun `provider connection modes map to matching card modes`() {
|
||||
assertEquals(
|
||||
TrackingConnectionCardMode.DISCONNECTED,
|
||||
TraktConnectionMode.DISCONNECTED.toTrackingConnectionCardMode(),
|
||||
)
|
||||
assertEquals(
|
||||
TrackingConnectionCardMode.AWAITING_APPROVAL,
|
||||
TraktConnectionMode.AWAITING_APPROVAL.toTrackingConnectionCardMode(),
|
||||
)
|
||||
assertEquals(
|
||||
TrackingConnectionCardMode.CONNECTED,
|
||||
TraktConnectionMode.CONNECTED.toTrackingConnectionCardMode(),
|
||||
)
|
||||
assertEquals(
|
||||
TrackingConnectionCardMode.DISCONNECTED,
|
||||
SimklConnectionMode.DISCONNECTED.toTrackingConnectionCardMode(),
|
||||
)
|
||||
assertEquals(
|
||||
TrackingConnectionCardMode.AWAITING_APPROVAL,
|
||||
SimklConnectionMode.AWAITING_APPROVAL.toTrackingConnectionCardMode(),
|
||||
)
|
||||
assertEquals(
|
||||
TrackingConnectionCardMode.CONNECTED,
|
||||
SimklConnectionMode.CONNECTED.toTrackingConnectionCardMode(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `remote brands are available only while their provider is connected`() {
|
||||
assertTrue(isTrackingBrandAvailable(TrackingBrand.NUVIO, false, false))
|
||||
assertTrue(isTrackingBrandAvailable(TrackingBrand.TMDB, false, false))
|
||||
assertFalse(isTrackingBrandAvailable(TrackingBrand.TRAKT, false, true))
|
||||
assertTrue(isTrackingBrandAvailable(TrackingBrand.TRAKT, true, false))
|
||||
assertFalse(isTrackingBrandAvailable(TrackingBrand.SIMKL, true, false))
|
||||
assertTrue(isTrackingBrandAvailable(TrackingBrand.SIMKL, false, true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Trakt recommendations fall back visually without rewriting preference`() {
|
||||
val stored = MoreLikeThisSourcePreference.TRAKT
|
||||
|
||||
assertEquals(
|
||||
MoreLikeThisSourcePreference.TMDB,
|
||||
effectiveTrackingRecommendationsSource(stored, traktConnected = false),
|
||||
)
|
||||
assertEquals(
|
||||
MoreLikeThisSourcePreference.TRAKT,
|
||||
effectiveTrackingRecommendationsSource(stored, traktConnected = true),
|
||||
)
|
||||
assertEquals(MoreLikeThisSourcePreference.TRAKT, stored)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.nuvio.app.features.simkl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import nuvio.composeapp.generated.resources.Res
|
||||
import nuvio.composeapp.generated.resources.simkl_logo_glyph
|
||||
import nuvio.composeapp.generated.resources.simkl_logo_wordmark
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
|
||||
@Composable
|
||||
actual fun simklBrandPainter(asset: SimklBrandAsset): Painter =
|
||||
when (asset) {
|
||||
SimklBrandAsset.Glyph -> painterResource(Res.drawable.simkl_logo_glyph)
|
||||
SimklBrandAsset.Wordmark -> painterResource(Res.drawable.simkl_logo_wordmark)
|
||||
}
|
||||
Loading…
Reference in a new issue