new icons, album page, blocklevel smart lists
44
assets/icons/exclude_always.svg
Normal file
|
@ -0,0 +1,44 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
enable-background="new 0 0 20 20"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
fill="#000000"
|
||||
version="1.1"
|
||||
id="svg10"
|
||||
sodipodi:docname="exclude_always.svg"
|
||||
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview6"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
scale-x="1"
|
||||
inkscape:zoom="24.719275"
|
||||
inkscape:cx="9.2235717"
|
||||
inkscape:cy="9.6281143"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1011"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="32"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg10" />
|
||||
<defs
|
||||
id="defs14" />
|
||||
<g
|
||||
id="g8"
|
||||
transform="matrix(1.2868381,0,0,1.2868381,-0.86194759,-0.86838178)">
|
||||
<path
|
||||
id="path6"
|
||||
d="M 12.27,3 H 7.73 C 7.33,3 6.95,3.16 6.67,3.44 L 3.44,6.67 C 3.16,6.95 3,7.34 3,7.73 v 4.53 c 0,0.4 0.16,0.78 0.44,1.06 l 3.23,3.23 C 6.95,16.84 7.34,17 7.73,17 h 4.53 c 0.4,0 0.78,-0.16 1.06,-0.44 l 3.23,-3.23 c 0.28,-0.28 0.44,-0.66 0.44,-1.06 V 7.73 c 0,-0.4 -0.16,-0.78 -0.44,-1.06 L 13.32,3.44 C 13.05,3.16 12.66,3 12.27,3 Z m 0.48,9.75 v 0 c -0.29,0.29 -0.77,0.29 -1.06,0 C 7.25,8.31 10.898285,11.958285 7.25,8.31 6.96,8.02 6.96,7.54 7.25,7.25 v 0 c 0.29,-0.29 0.77,-0.29 1.06,0 4.44,4.44 0,0 4.44,4.44 0.29,0.29 0.29,0.77 0,1.06 z" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
51
assets/icons/exclude_never.svg
Normal file
|
@ -0,0 +1,51 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
enable-background="new 0 0 20 20"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
fill="#000000"
|
||||
version="1.1"
|
||||
id="svg10"
|
||||
sodipodi:docname="exclude_never.svg"
|
||||
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
scale-x="1"
|
||||
inkscape:zoom="17.479167"
|
||||
inkscape:cx="11.041716"
|
||||
inkscape:cy="13.902265"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1011"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="32"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg10" />
|
||||
<defs
|
||||
id="defs14" />
|
||||
<g
|
||||
id="g8-3"
|
||||
transform="translate(0.005)"
|
||||
style="fill:#ff0000;fill-opacity:1" />
|
||||
<g
|
||||
id="layer1"
|
||||
style="display:inline"
|
||||
transform="matrix(1.2861154,0,0,1.2861154,-0.85435141,-0.86115434)">
|
||||
<path
|
||||
id="path6"
|
||||
style="fill:#000000;fill-opacity:1;stroke-width:2.4"
|
||||
transform="scale(0.41666667)"
|
||||
d="m 18.552734,7.1992188 c -0.96,0 -1.872922,0.3846406 -2.544922,1.0566406 L 8.2558594,16.007812 c -0.672,0.672001 -1.0566406,1.608922 -1.0566406,2.544922 v 10.871094 c 0,0.96 0.3846406,1.872922 1.0566406,2.544922 l 7.7519526,7.751953 c 0.672001,0.696 1.608922,1.080078 2.544922,1.080078 h 10.871094 c 0.96,0 1.872922,-0.38464 2.544922,-1.05664 l 7.751953,-7.751953 c 0.672,-0.672 1.054688,-1.584922 1.054688,-2.544922 V 18.552734 c 0,-0.96 -0.382688,-1.872922 -1.054688,-2.544922 L 31.96875,8.2558594 c -0.648,-0.672 -1.585484,-1.0566406 -2.521484,-1.0566406 z m 1.492188,2.0136718 c 1.079369,-0.00998 1.081248,0.03125 9.722656,0.03125 L 38.744141,18.234375 38.65625,30.060547 30.3125,38.521484 17.669922,38.503906 9.3457031,30.037109 9.2558594,18.238281 18.246094,9.2441406 c 0.958723,-0.017736 1.439038,-0.027924 1.798828,-0.03125 z" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.2 KiB |
54
assets/icons/exclude_shuffle.svg
Normal file
|
@ -0,0 +1,54 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
enable-background="new 0 0 20 20"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
fill="#000000"
|
||||
version="1.1"
|
||||
id="svg10"
|
||||
sodipodi:docname="exclude_shuffle.svg"
|
||||
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview8"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
inkscape:zoom="12.359637"
|
||||
inkscape:cx="11.812644"
|
||||
inkscape:cy="17.880784"
|
||||
inkscape:window-width="1338"
|
||||
inkscape:window-height="979"
|
||||
inkscape:window-x="578"
|
||||
inkscape:window-y="60"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg10" />
|
||||
<defs
|
||||
id="defs14" />
|
||||
<g
|
||||
id="layer1"
|
||||
style="display:inline"
|
||||
transform="matrix(1.2900203,0,0,1.2900203,-0.89337981,-0.90020339)">
|
||||
<path
|
||||
id="path6"
|
||||
style="stroke-width:2.4"
|
||||
transform="scale(0.41666667)"
|
||||
d="m 18.552734,7.1992188 c -0.96,0 -1.872922,0.3846406 -2.544922,1.0566406 L 8.2558594,16.007812 c -0.672,0.672001 -1.0566406,1.608922 -1.0566406,2.544922 v 10.871094 c 0,0.96 0.3846406,1.872922 1.0566406,2.544922 l 7.7519526,7.751953 c 0.672001,0.696 1.608922,1.080078 2.544922,1.080078 h 10.871094 c 0.96,0 1.872922,-0.38464 2.544922,-1.05664 l 7.751953,-7.751953 c 0.672,-0.672 1.054688,-1.584922 1.054688,-2.544922 V 18.552734 c 0,-0.96 -0.382688,-1.872922 -1.054688,-2.544922 L 31.96875,8.2558594 c -0.648,-0.672 -1.585484,-1.0566406 -2.521484,-1.0566406 z m 9.087891,8.3125002 h 3.935547 c 0.386383,0 0.701172,0.314789 0.701172,0.701172 v 3.933593 c 0,0.306614 -0.190177,0.549115 -0.431641,0.650391 -0.241471,0.101275 -0.547953,0.06923 -0.761719,-0.144531 L 29.978516,19.544922 17.802734,31.697266 c -0.47449,0.474492 -1.250118,0.474492 -1.724609,0 -0.474276,-0.474274 -0.474275,-1.246427 0,-1.720704 L 28.244141,17.810547 27.136719,16.705078 c -0.213768,-0.213765 -0.247772,-0.520247 -0.146485,-0.761719 0.10128,-0.241471 0.343777,-0.43164 0.650391,-0.43164 z m -10.691406,0.423828 c 0.31061,0 0.625699,0.115935 0.863281,0.353515 l 4.640625,4.642579 a 0.18262874,0.18262874 0 0 1 -0.002,0.259765 l -1.472656,1.453125 a 0.18262874,0.18262874 0 0 1 -0.257813,0 l -4.630859,-4.630859 c -0.474491,-0.474491 -0.474491,-1.250118 0,-1.72461 0.237151,-0.237148 0.548764,-0.353515 0.859375,-0.353515 z m 9.777343,9.345703 a 0.18262874,0.18262874 0 0 1 0.13086,0.05273 l 3.121094,3.13086 1.105468,-1.107422 c 0.212376,-0.217416 0.518093,-0.254 0.759766,-0.154297 0.242465,0.100032 0.433594,0.343776 0.433594,0.650391 v 3.933593 c 0,0.386384 -0.314789,0.701172 -0.701172,0.701172 h -3.935547 c -0.306614,0 -0.549113,-0.190176 -0.650391,-0.43164 -0.10128,-0.241472 -0.06726,-0.547954 0.146485,-0.761719 l 1.117187,-1.117188 -3.121094,-3.11914 a 0.18262874,0.18262874 0 0 1 0,-0.259766 l 1.464844,-1.464844 a 0.18262874,0.18262874 0 0 1 0.128906,-0.05273 z" />
|
||||
</g>
|
||||
<g
|
||||
id="layer2"
|
||||
style="display:inline">
|
||||
<g
|
||||
style="fill:#ff0000;fill-opacity:1"
|
||||
id="g1247"
|
||||
transform="matrix(0.43263311,0,0,0.43263311,4.7202537,4.8084027)" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.4 KiB |
25
assets/icons/exclude_shuffle_all.svg
Normal file
|
@ -0,0 +1,25 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
enable-background="new 0 0 20 20"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
fill="#000000"
|
||||
version="1.1"
|
||||
id="svg10"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs14" />
|
||||
<g
|
||||
id="layer1"
|
||||
style="display:inline"
|
||||
transform="matrix(1.2870502,0,0,1.2870502,-0.86105704,-0.86105704)">
|
||||
<g
|
||||
id="g8" />
|
||||
<path
|
||||
id="path829"
|
||||
style="fill:#000000;fill-opacity:1;stroke-width:0.381514"
|
||||
d="m 17.28035,14.243237 c -0.763028,-0.228908 -1.548946,-0.01526 -2.056359,0.492153 l -1.079684,0.957599 -0.579901,0.511229 h 0.0038 l -1.026291,0.911817 c -0.309026,0.309026 -0.743952,0.438741 -1.190324,0.350993 -0.476891,-0.09538 -0.86985,-0.476891 -0.980489,-0.94997 -0.198387,-0.850774 0.442556,-1.602357 1.25518,-1.602357 0.347177,0 0.671465,0.13353 0.930893,0.392959 l 0.179312,0.156422 c 0.144975,0.129713 0.362438,0.129713 0.507414,0 0.17168,-0.152606 0.17168,-0.419666 0,-0.572272 l -0.160203,-0.137345 c -0.389144,-0.389143 -0.908003,-0.602792 -1.457382,-0.602792 -1.136911,0 -2.060175,0.923263 -2.060175,2.052545 0,1.129281 0.923264,2.052544 2.060175,2.052544 0.549379,0 1.068238,-0.213649 1.438306,-0.583717 l 1.079685,-0.953783 0.0038,0.0038 0.576086,-0.518859 h -0.0038 l 1.026272,-0.911818 c 0.309026,-0.309026 0.743952,-0.438741 1.190324,-0.350993 0.476891,0.09538 0.86985,0.476893 0.98049,0.94997 0.198387,0.850776 -0.442557,1.602357 -1.25518,1.602357 -0.343362,0 -0.671465,-0.133529 -0.930894,-0.392959 l -0.183128,-0.160258 c -0.144975,-0.129716 -0.362438,-0.129716 -0.507412,0 -0.171682,0.152606 -0.171682,0.419665 0,0.57227 l 0.160235,0.14116 c 0.389144,0.38533 0.904188,0.598976 1.457382,0.598976 1.247551,0 2.235672,-1.106389 2.033468,-2.38446 -0.114448,-0.759212 -0.675279,-1.407786 -1.4116,-1.625249 z M 11.522217,6.4632163 h 1.639811 c 0.160992,0 0.292155,0.1311621 0.292155,0.292155 v 1.6389971 c 0,0.1277558 -0.07924,0.2287979 -0.17985,0.2709963 -0.100613,0.042198 -0.228315,0.028846 -0.317383,-0.060221 L 12.496338,8.1437176 7.4230955,13.207194 c -0.1977042,0.197705 -0.5208825,0.197705 -0.7185871,0 -0.197615,-0.197615 -0.1976146,-0.519345 0,-0.71696 L 11.773682,7.4210613 11.312256,6.9604492 c -0.08907,-0.089069 -0.103234,-0.2167696 -0.06104,-0.3173829 0.0422,-0.1006129 0.143241,-0.17985 0.270997,-0.17985 z m 0,0 h 1.639811 c 0.160992,0 0.292155,0.1311621 0.292155,0.292155 v 1.6389971 c 0,0.1277558 -0.07924,0.2287979 -0.17985,0.2709963 -0.100613,0.042198 -0.228315,0.028846 -0.317383,-0.060221 L 12.496338,8.1437176 7.4230955,13.207194 c -0.1977042,0.197705 -0.5208825,0.197705 -0.7185871,0 -0.197615,-0.197615 -0.1976146,-0.519345 0,-0.71696 L 11.773682,7.4210613 11.312256,6.9604492 c -0.08907,-0.089069 -0.103234,-0.2167696 -0.06104,-0.3173829 0.0422,-0.1006129 0.143241,-0.17985 0.270997,-0.17985 z m -4.4547528,0.176595 c 0.1294209,0 0.260708,0.048306 0.3597005,0.1472979 l 1.9335937,1.934408 c 0.029905,0.030018 0.02953,0.078681 -8.333e-4,0.1082352 l -0.6136067,0.605469 c -0.029695,0.029589 -0.077727,0.029589 -0.1074221,0 L 6.7093717,7.5056967 c -0.1977045,-0.1977046 -0.1977045,-0.5208829 0,-0.7185875 0.098812,-0.098813 0.2286517,-0.1472978 0.358073,-0.1472978 z m 4.0738928,3.8940427 c 0.02038,-2.75e-4 0.04002,0.0076 0.05452,0.02197 l 1.300456,1.304525 0.460612,-0.461426 c 0.08848,-0.09059 0.215871,-0.105833 0.316569,-0.06429 0.101027,0.04168 0.180664,0.143241 0.180664,0.270997 v 1.638997 c 0,0.160993 -0.131163,0.292155 -0.292155,0.292155 h -1.639811 c -0.127756,0 -0.228798,-0.07924 -0.270997,-0.17985 -0.0422,-0.100614 -0.02803,-0.228314 0.06104,-0.317383 l 0.465495,-0.465495 -1.300455,-1.299642 c -0.03013,-0.02979 -0.03013,-0.07845 0,-0.108236 l 0.610351,-0.610351 c 0.0143,-0.01413 0.03361,-0.02203 0.05371,-0.02197 z M 7.7303059,2.9996745 c -0.4,0 -0.7803838,0.1602669 -1.0603842,0.4402669 L 3.4399414,6.6699217 c -0.28,0.2800009 -0.4402669,0.6703842 -0.4402669,1.0603842 v 4.5296221 c 0,0.4 0.1602669,0.780385 0.4402669,1.060385 l 3.2299803,3.22998 c 0.2800009,0.29 0.6703842,0.450033 1.0603842,0.450033 H 8.8252442 C 8.6610047,16.700722 8.6493063,16.401739 8.6066555,16.048177 L 7.3624676,16.043294 3.894043,12.515462 3.8566081,7.5992838 7.6025392,3.8517253 c 0.3994679,-0.00739 0.5995992,-0.011635 0.7495117,-0.013021 0.4497371,-0.00416 0.4505204,0.013021 4.0511071,0.013021 l 3.740234,3.745931 -0.03662,4.9275717 -0.90011,0.996928 c -0.0038,0.01748 0.0026,0.02969 0.0245,0.03399 l 1.091334,-0.01621 c 0.356403,-0.321284 0.655216,-0.853668 0.667295,-1.270237 V 7.7303059 c 0,-0.4 -0.159453,-0.7803838 -0.439453,-1.0603842 L 13.320313,3.4399414 c -0.27,-0.28 -0.660619,-0.4402669 -1.050619,-0.4402669 z" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4.6 KiB |
16
assets/icons/favorite_1_3.svg
Normal file
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
width="24px"
|
||||
fill="#000000"
|
||||
version="1.1"
|
||||
id="svg6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
id="path4"
|
||||
d="M 7.4589844,3 C 6.4064062,3.0070898 5.3298438,3.3114844 4.3398438,3.9902344 2.9398438,4.9502344 2.06,6.5692969 2,8.2792969 1.86,12.159297 5.3007812,15.269062 10.550781,20.039062 l 0.09961,0.0918 c 0.76,0.69 1.929453,0.688282 2.689453,-0.01172 l 0.109375,-0.09961 c 5.25,-4.76 8.680781,-7.87 8.550781,-11.7499998 -0.06,-1.7 -0.939844,-3.3192968 -2.339844,-4.2792968 C 17.020156,2.1902344 13.76,3.0298437 12,5.0898438 10.9,3.8023438 9.2132812,2.9881836 7.4589844,3 Z M 7.5,5 c 1.54,0 3.040312,0.989375 3.570312,2.359375 h 1.869141 C 13.459453,5.989375 14.96,5 16.5,5 18.5,5 20,6.5 20,8.5 20,9.9474247 19.21244,11.384197 17.828125,13 H 6.171875 C 4.7875597,11.384197 4,9.9474247 4,8.5 4,6.5 5.5,5 7.5,5 Z" />
|
||||
<defs
|
||||
id="defs10" />
|
||||
</svg>
|
After Width: | Height: | Size: 1,018 B |
16
assets/icons/favorite_2_3.svg
Normal file
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
width="24px"
|
||||
fill="#000000"
|
||||
version="1.1"
|
||||
id="svg6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
id="path4"
|
||||
d="M 7.4589844,3 C 6.4064062,3.0070898 5.3298438,3.3114844 4.3398438,3.9902344 2.9398438,4.9502344 2.06,6.5692969 2,8.2792969 1.86,12.159297 5.3007812,15.269062 10.550781,20.039062 l 0.09961,0.0918 c 0.76,0.69 1.929453,0.688282 2.689453,-0.01172 l 0.109375,-0.09961 c 5.25,-4.76 8.680781,-7.87 8.550781,-11.7499998 -0.06,-1.7 -0.939844,-3.3192968 -2.339844,-4.2792968 C 17.020156,2.1902344 13.76,3.0298437 12,5.0898438 10.9,3.8023438 9.2132812,2.9881836 7.4589844,3 Z M 7.5,5 c 1.54,0 3.040312,0.989375 3.570312,2.359375 h 1.869141 C 13.459453,5.989375 14.96,5 16.5,5 c 2,0 3.5,1.5 3.5,3.5 0,0.1668904 -0.01255,0.3330876 -0.0332,0.5 H 4.0332031 C 4.0125534,8.8330876 4,8.6668904 4,8.5 4,6.5 5.5,5 7.5,5 Z" />
|
||||
<defs
|
||||
id="defs10" />
|
||||
</svg>
|
After Width: | Height: | Size: 1,018 B |
39
assets/icons/shuffle_heart.svg
Normal file
|
@ -0,0 +1,39 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
fill="#000000"
|
||||
version="1.1"
|
||||
id="svg4"
|
||||
sodipodi:docname="shuffle_heart.svg"
|
||||
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview5"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
inkscape:zoom="24.719275"
|
||||
inkscape:cx="11.165376"
|
||||
inkscape:cy="12.985818"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1011"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="32"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg4" />
|
||||
<defs
|
||||
id="defs8" />
|
||||
<path
|
||||
id="path2"
|
||||
style="stroke-width:1"
|
||||
d="M 12.832531,0.6816406 C 12.433671,0.67895405 12.050403,0.86449835 11.800305,1.1572265 11.400148,0.6888614 10.659329,0.4979755 10.059094,0.90722655 9.7385142,1.1254938 9.5405087,1.4962976 9.5268672,1.8828125 9.4973122,2.7649759 10.276579,3.471786 11.470227,4.5517578 l 0.02539,0.022461 c 0.172795,0.1591496 0.438533,0.1598093 0.611328,0.00293 l 0.02344,-0.020508 C 13.324032,3.4721253 14.105572,2.7649759 14.073742,1.8828125 14.0601,1.4962976 13.862096,1.1254938 13.541516,0.90722655 13.316428,0.7529048 13.071847,0.6832526 12.832531,0.6816406 Z M 15.510266,4 c -0.45,0 -0.670352,0.5396093 -0.360352,0.8496094 L 16.340344,6.040039 4.5005,17.879883 c -0.39,0.39 -0.39,1.020156 0,1.410156 0.3900002,0.39 1.0201562,0.39 1.4101562,0 L 17.760266,7.459961 18.950695,8.6503905 c 0.31,0.31 0.84961,0.089648 0.84961,-0.3603515 V 4.5 c 0,-0.28 -0.22,-0.5 -0.5,-0.5 z M 5.2153437,4.4072265 c -0.255,0 -0.510078,0.097969 -0.705078,0.2929688 -0.39,0.3900002 -0.39,1.0201562 0,1.4101562 L 8.9702267,10.570313 10.390149,9.169922 5.9204217,4.7001953 c -0.195,-0.195 -0.450078,-0.2929688 -0.705078,-0.2929688 z m 9.4150393,9.0029295 -1.410156,1.410157 3.129882,3.129882 -1.200195,1.200195 c -0.31,0.31 -0.08965,0.84961 0.360352,0.84961 h 3.790039 c 0.28,0 0.5,-0.22 0.5,-0.5 v -3.790039 c 0,-0.45 -0.53961,-0.669609 -0.84961,-0.349609 l -1.190429,1.189452 z" />
|
||||
</svg>
|
After Width: | Height: | Size: 2.4 KiB |
BIN
fonts/MuckeIcons.ttf
Normal file
|
@ -1,5 +1,3 @@
|
|||
import 'package:mucke/domain/entities/playable.dart';
|
||||
|
||||
const int MAX_LIKE_COUNT = 3;
|
||||
|
||||
const String PERSISTENT_INDEX = 'PERSISTENT_INDEX';
|
||||
|
@ -8,4 +6,4 @@ const String PERSISTENT_SHUFFLEMODE = 'PERSISTENT_SHUFFLEMODE';
|
|||
const String PERSISTENT_PLAYABLE = 'PERSISTENT_PLAYABLE';
|
||||
|
||||
const String SETTING_EXCLUDE_SKIPPED_SONGS = 'SETTING_EXCLUDE_SKIPPED_SONGS_ENABLED';
|
||||
const String SETTING_SKIP_THRESHOLD = 'SETTING_SKIP_THRESHOLD';
|
||||
const String SETTING_SKIP_THRESHOLD = 'SETTING_SKIP_THRESHOLD';
|
||||
|
|
|
@ -38,7 +38,7 @@ class Filter extends Equatable {
|
|||
this.maxSkipCount,
|
||||
this.minYear,
|
||||
this.maxYear,
|
||||
required this.excludeBlocked,
|
||||
required this.blockLevel,
|
||||
this.limit,
|
||||
});
|
||||
|
||||
|
@ -57,7 +57,7 @@ class Filter extends Equatable {
|
|||
final int? minYear;
|
||||
final int? maxYear;
|
||||
|
||||
final bool excludeBlocked;
|
||||
final int blockLevel;
|
||||
|
||||
final int? limit;
|
||||
|
||||
|
|
|
@ -73,6 +73,10 @@ class DynamicQueue implements ManagedQueueInfo {
|
|||
|
||||
final initialQueueItem = _availableSongs[startIndex];
|
||||
|
||||
// FIXME: broken when first song is filtered out
|
||||
// when starting a shuffle playback, there needs to be an option to select a new startindex
|
||||
// in this case the startindex was selected randomly
|
||||
// otherwise the startindex item should be included as it was selected manually
|
||||
final filteredAvailableSongs = _filterAvailableSongs(_availableSongs);
|
||||
final newStartIndex = filteredAvailableSongs.indexOf(initialQueueItem);
|
||||
|
||||
|
|
|
@ -1,10 +1,5 @@
|
|||
import 'package:rxdart/rxdart.dart';
|
||||
|
||||
abstract class SettingsInfoRepository {
|
||||
Stream<List<String>> get libraryFoldersStream;
|
||||
|
||||
ValueStream<bool> get isBlockSkippedSongsEnabled;
|
||||
ValueStream<int> get blockSkippedSongsThreshold;
|
||||
}
|
||||
|
||||
abstract class SettingsRepository extends SettingsInfoRepository {
|
||||
|
|
20
lib/presentation/mucke_icons.dart
Normal file
|
@ -0,0 +1,20 @@
|
|||
/// Flutter icons MuckeIcons
|
||||
/// Copyright (C) 2022 by original authors @ fluttericon.com, fontello.com
|
||||
/// This font was generated by FlutterIcon.com, which is derived from Fontello.
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class MuckeIcons {
|
||||
MuckeIcons._();
|
||||
|
||||
static const _kFontFam = 'MuckeIcons';
|
||||
static const String? _kFontPkg = null;
|
||||
|
||||
static const IconData favorite_1_3 = IconData(0xe800, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData favorite_2_3 = IconData(0xe801, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData exclude_shuffle = IconData(0xe802, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData exclude_shuffle_all = IconData(0xe803, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData exclude_never = IconData(0xe804, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData exclude_always = IconData(0xe805, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData shuffle_heart = IconData(0xe806, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
}
|
|
@ -7,9 +7,9 @@ import '../../domain/entities/song.dart';
|
|||
import '../state/album_page_store.dart';
|
||||
import '../state/audio_store.dart';
|
||||
import '../theming.dart';
|
||||
import '../utils.dart';
|
||||
import '../widgets/album_sliver_appbar.dart';
|
||||
import '../widgets/song_bottom_sheet.dart';
|
||||
import '../widgets/song_list_tile.dart';
|
||||
|
||||
class AlbumDetailsPage extends StatefulWidget {
|
||||
const AlbumDetailsPage({Key? key, required this.album}) : super(key: key);
|
||||
|
@ -48,6 +48,14 @@ class _AlbumDetailsPageState extends State<AlbumDetailsPage> {
|
|||
slivers: <Widget>[
|
||||
AlbumSliverAppBar(
|
||||
album: widget.album,
|
||||
songs: store.albumSongStream.value ?? [],
|
||||
),
|
||||
SliverList(
|
||||
delegate: SliverChildListDelegate([
|
||||
// const SizedBox(
|
||||
// height: 24.0,
|
||||
// ),
|
||||
]),
|
||||
),
|
||||
for (int d = 0; d < songsByDisc.length; d++)
|
||||
SliverList(
|
||||
|
@ -57,8 +65,8 @@ class _AlbumDetailsPageState extends State<AlbumDetailsPage> {
|
|||
if (songsByDisc.length > 1)
|
||||
ListTile(
|
||||
title: Text('Disc ${d + 1}', style: TEXT_HEADER),
|
||||
leading: const SizedBox(width: 56, child: Icon(Icons.album)),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
leading: const SizedBox(width: 40, child: Icon(Icons.album)),
|
||||
contentPadding: const EdgeInsets.only(left: HORIZONTAL_PADDING),
|
||||
),
|
||||
if (songsByDisc.length > 1)
|
||||
Padding(
|
||||
|
@ -71,16 +79,58 @@ class _AlbumDetailsPageState extends State<AlbumDetailsPage> {
|
|||
),
|
||||
),
|
||||
for (int s = 0; s < songsByDisc[d].length; s++)
|
||||
SongListTile(
|
||||
song: songsByDisc[d][s],
|
||||
showAlbum: false,
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.only(left: HORIZONTAL_PADDING),
|
||||
leading: SizedBox(
|
||||
height: 56,
|
||||
width: 40,
|
||||
child: Center(child: Text('${songsByDisc[d][s].trackNumber}')),
|
||||
),
|
||||
title: Text(
|
||||
songsByDisc[d][s].title,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: Text(
|
||||
'${msToTimeString(songsByDisc[d][s].duration)} • ${songsByDisc[d][s].artist}',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w300,
|
||||
),
|
||||
),
|
||||
onTap: () => audioStore.playSong(
|
||||
s + _calcOffset(d, songsByDisc),
|
||||
store.albumSongStream.value!,
|
||||
widget.album,
|
||||
),
|
||||
onTapMore: () => SongBottomSheet()(songsByDisc[d][s], context),
|
||||
)
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (songsByDisc[d][s].blockLevel > 0)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 4.0),
|
||||
child: Icon(
|
||||
blockLevelIcon(songsByDisc[d][s].blockLevel),
|
||||
size: 16.0,
|
||||
color: Colors.white38,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
likeCountIcon(songsByDisc[d][s].likeCount),
|
||||
size: 16.0,
|
||||
color: songsByDisc[d][s].likeCount == 3
|
||||
? LIGHT2
|
||||
: Colors.white.withOpacity(
|
||||
0.2 + 0.18 * songsByDisc[d][s].likeCount,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.more_vert),
|
||||
iconSize: 20.0,
|
||||
onPressed: () => SongBottomSheet()(songsByDisc[d][s], context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
|
|
@ -11,6 +11,7 @@ import '../theming.dart';
|
|||
import '../widgets/album_art.dart';
|
||||
import '../widgets/album_background.dart';
|
||||
import '../widgets/currently_playing_header.dart';
|
||||
import '../widgets/custom_modal_bottom_sheet.dart';
|
||||
import '../widgets/playback_control.dart';
|
||||
import '../widgets/song_customization_buttons.dart';
|
||||
import '../widgets/song_info.dart';
|
||||
|
@ -44,10 +45,7 @@ class CurrentlyPlayingPage extends StatelessWidget {
|
|||
builder: (BuildContext context) {
|
||||
_log.d('Observer.build');
|
||||
final Song? song = audioStore.currentSongStream.value;
|
||||
|
||||
if (song == null) {
|
||||
return Container();
|
||||
}
|
||||
if (song == null) return Container();
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
|
@ -87,26 +85,24 @@ class CurrentlyPlayingPage extends StatelessWidget {
|
|||
flex: 50,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 12.0, right: 12.0),
|
||||
child: Center(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
song.title,
|
||||
overflow: TextOverflow.fade,
|
||||
softWrap: false,
|
||||
maxLines: 1,
|
||||
style: TEXT_BIG,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
song.title,
|
||||
overflow: TextOverflow.fade,
|
||||
softWrap: false,
|
||||
maxLines: 1,
|
||||
style: TEXT_BIG,
|
||||
),
|
||||
Text(
|
||||
'${song.artist} • ${song.album}',
|
||||
style: TEXT_SUBTITLE.copyWith(
|
||||
color: Colors.grey[100],
|
||||
),
|
||||
Text(
|
||||
song.artist,
|
||||
style: TEXT_SUBTITLE.copyWith(
|
||||
color: Colors.grey[100],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Spacer(
|
||||
|
@ -168,87 +164,72 @@ class CurrentlyPlayingPage extends StatelessWidget {
|
|||
final artists = await musicDataStore.artistStream.first;
|
||||
final artist = artists.singleWhere((a) => a.name == album.artist);
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
backgroundColor: DARK2,
|
||||
builder: (context) {
|
||||
return Container(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
height: 2,
|
||||
color: LIGHT1,
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
ListTile(
|
||||
title: const Text('Show song info'),
|
||||
leading: const Icon(Icons.info),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return SimpleDialog(
|
||||
backgroundColor: DARK3,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(HORIZONTAL_PADDING),
|
||||
child: SongInfo(song),
|
||||
),
|
||||
SimpleDialogOption(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text(
|
||||
'Close',
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('Go to artist'),
|
||||
leading: const Icon(Icons.person_rounded),
|
||||
trailing: const Icon(Icons.chevron_right_rounded),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context);
|
||||
navStore.pushOnLibrary(
|
||||
MaterialPageRoute<Widget>(
|
||||
builder: (BuildContext context) => ArtistDetailsPage(
|
||||
artist: artist,
|
||||
CustomModalBottomSheet()(
|
||||
context,
|
||||
[
|
||||
ListTile(
|
||||
title: const Text('Show song info'),
|
||||
leading: const Icon(Icons.info),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return SimpleDialog(
|
||||
backgroundColor: DARK3,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(HORIZONTAL_PADDING),
|
||||
child: SongInfo(song),
|
||||
),
|
||||
SimpleDialogOption(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text(
|
||||
'Close',
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('Go to artist'),
|
||||
leading: const Icon(Icons.person_rounded),
|
||||
trailing: const Icon(Icons.chevron_right_rounded),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context);
|
||||
navStore.pushOnLibrary(
|
||||
MaterialPageRoute<Widget>(
|
||||
builder: (BuildContext context) => ArtistDetailsPage(
|
||||
artist: artist,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('Go to album'),
|
||||
leading: const Icon(Icons.album_rounded),
|
||||
trailing: const Icon(Icons.chevron_right_rounded),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context);
|
||||
navStore.pushOnLibrary(
|
||||
MaterialPageRoute<Widget>(
|
||||
builder: (BuildContext context) => AlbumDetailsPage(
|
||||
album: album,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('Go to album'),
|
||||
leading: const Icon(Icons.album_rounded),
|
||||
trailing: const Icon(Icons.chevron_right_rounded),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context);
|
||||
navStore.pushOnLibrary(
|
||||
MaterialPageRoute<Widget>(
|
||||
builder: (BuildContext context) => AlbumDetailsPage(
|
||||
album: album,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,6 +40,13 @@ class _SmartListFormPageState extends State<SmartListFormPage> {
|
|||
final title = widget.smartList == null ? 'Create smart list' : 'Edit smart list';
|
||||
final SettingsStore settingsStore = GetIt.I<SettingsStore>();
|
||||
|
||||
const blockLevelTexts = <String>[
|
||||
'Exclude all songs marked for exclusion.',
|
||||
'Exclude songs marked for exclusion in shuffle mode.',
|
||||
'Exclude only songs marked as always exclude.',
|
||||
"Don't exclude any songs.",
|
||||
];
|
||||
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
|
@ -364,15 +371,22 @@ class _SmartListFormPageState extends State<SmartListFormPage> {
|
|||
padding: const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING),
|
||||
child: Observer(
|
||||
builder: (_) {
|
||||
return Row(
|
||||
children: [
|
||||
Switch(
|
||||
value: store.excludeBlocked,
|
||||
onChanged: (bool value) => store.excludeBlocked = value,
|
||||
),
|
||||
const Text('Exclude blocked songs'),
|
||||
const Spacer(),
|
||||
],
|
||||
return DropdownButton<int>(
|
||||
value: store.blockLevel,
|
||||
hint: const Text('Select which songs to exclude.'),
|
||||
isExpanded: true,
|
||||
onChanged: (int? newValue) {
|
||||
if (newValue != null) store.blockLevel = newValue;
|
||||
},
|
||||
items: <int>[0, 1, 2, 3].map<DropdownMenuItem<int>>((int value) {
|
||||
return DropdownMenuItem<int>(
|
||||
value: value,
|
||||
child: Text(
|
||||
blockLevelTexts[value],
|
||||
style: const TextStyle(fontSize: 14.0),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
|
|
@ -7,7 +7,6 @@ import '../../domain/entities/playable.dart';
|
|||
import '../../domain/entities/song.dart';
|
||||
import '../state/audio_store.dart';
|
||||
import '../state/music_data_store.dart';
|
||||
import '../state/settings_store.dart';
|
||||
import '../widgets/song_bottom_sheet.dart';
|
||||
import '../widgets/song_list_tile.dart';
|
||||
|
||||
|
@ -24,15 +23,12 @@ class _SongsPageState extends State<SongsPage> with AutomaticKeepAliveClientMixi
|
|||
print('SongsPage.build');
|
||||
final MusicDataStore musicDataStore = GetIt.I<MusicDataStore>();
|
||||
final AudioStore audioStore = GetIt.I<AudioStore>();
|
||||
final SettingsStore settingsStore = GetIt.I<SettingsStore>();
|
||||
|
||||
super.build(context);
|
||||
return Observer(builder: (_) {
|
||||
print('SongsPage.build -> Observer.builder');
|
||||
|
||||
final songStream = musicDataStore.songStream;
|
||||
final isBlockSkippedSongsEnabled = settingsStore.isBlockSkippedSongsEnabled.first;
|
||||
final blockSkippedSongsThreshold = settingsStore.blockSkippedSongsThreshold.first;
|
||||
|
||||
switch (songStream.status) {
|
||||
case StreamStatus.active:
|
||||
|
@ -48,8 +44,6 @@ class _SongsPageState extends State<SongsPage> with AutomaticKeepAliveClientMixi
|
|||
subtitle: Subtitle.artistAlbum,
|
||||
onTap: () => audioStore.playSong(index, songs, AllSongs()),
|
||||
onTapMore: () => SongBottomSheet()(song, context),
|
||||
isBlockSkippedSongsEnabled: isBlockSkippedSongsEnabled.value,
|
||||
blockSkippedSongsThreshold: blockSkippedSongsThreshold.value,
|
||||
);
|
||||
},
|
||||
separatorBuilder: (BuildContext context, int index) => const SizedBox(
|
||||
|
|
|
@ -30,12 +30,6 @@ abstract class _SettingsStore with Store {
|
|||
late ObservableStream<List<String>> libraryFoldersStream =
|
||||
_settingsRepository.libraryFoldersStream.asObservable(initialValue: []);
|
||||
|
||||
@observable
|
||||
late ObservableStream<bool> isBlockSkippedSongsEnabled = _settingsRepository.isBlockSkippedSongsEnabled.asObservable();
|
||||
|
||||
@observable
|
||||
late ObservableStream<int> blockSkippedSongsThreshold = _settingsRepository.blockSkippedSongsThreshold.asObservable();
|
||||
|
||||
Future<void> addLibraryFolder(String? path) async {
|
||||
await _settingsRepository.addLibraryFolder(path);
|
||||
}
|
||||
|
|
|
@ -41,47 +41,11 @@ mixin _$SettingsStore on _SettingsStore, Store {
|
|||
});
|
||||
}
|
||||
|
||||
final _$isBlockSkippedSongsEnabledAtom =
|
||||
Atom(name: '_SettingsStore.isBlockSkippedSongsEnabled');
|
||||
|
||||
@override
|
||||
ObservableStream<bool> get isBlockSkippedSongsEnabled {
|
||||
_$isBlockSkippedSongsEnabledAtom.reportRead();
|
||||
return super.isBlockSkippedSongsEnabled;
|
||||
}
|
||||
|
||||
@override
|
||||
set isBlockSkippedSongsEnabled(ObservableStream<bool> value) {
|
||||
_$isBlockSkippedSongsEnabledAtom
|
||||
.reportWrite(value, super.isBlockSkippedSongsEnabled, () {
|
||||
super.isBlockSkippedSongsEnabled = value;
|
||||
});
|
||||
}
|
||||
|
||||
final _$blockSkippedSongsThresholdAtom =
|
||||
Atom(name: '_SettingsStore.blockSkippedSongsThreshold');
|
||||
|
||||
@override
|
||||
ObservableStream<int> get blockSkippedSongsThreshold {
|
||||
_$blockSkippedSongsThresholdAtom.reportRead();
|
||||
return super.blockSkippedSongsThreshold;
|
||||
}
|
||||
|
||||
@override
|
||||
set blockSkippedSongsThreshold(ObservableStream<int> value) {
|
||||
_$blockSkippedSongsThresholdAtom
|
||||
.reportWrite(value, super.blockSkippedSongsThreshold, () {
|
||||
super.blockSkippedSongsThreshold = value;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '''
|
||||
smartListsStream: ${smartListsStream},
|
||||
libraryFoldersStream: ${libraryFoldersStream},
|
||||
isBlockSkippedSongsEnabled: ${isBlockSkippedSongsEnabled},
|
||||
blockSkippedSongsThreshold: ${blockSkippedSongsThreshold}
|
||||
libraryFoldersStream: ${libraryFoldersStream}
|
||||
''';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -70,7 +70,7 @@ abstract class _SmartListStore with Store {
|
|||
late String limit = _intToString(_smartList?.filter.limit);
|
||||
|
||||
@observable
|
||||
late bool excludeBlocked = _smartList?.filter.excludeBlocked ?? false;
|
||||
late int blockLevel = _smartList?.filter.blockLevel ?? 0;
|
||||
|
||||
@observable
|
||||
late ObservableSet<Artist> selectedArtists =
|
||||
|
@ -204,7 +204,7 @@ abstract class _SmartListStore with Store {
|
|||
maxYear: maxYearEnabled ? int.tryParse(maxYear) : null,
|
||||
minLikeCount: minLikeCount,
|
||||
maxLikeCount: maxLikeCount,
|
||||
excludeBlocked: excludeBlocked,
|
||||
blockLevel: blockLevel,
|
||||
limit: limitEnabled ? int.tryParse(limit) : null,
|
||||
),
|
||||
orderBy: OrderBy(
|
||||
|
@ -230,7 +230,7 @@ abstract class _SmartListStore with Store {
|
|||
maxYear: maxYearEnabled ? int.tryParse(maxYear) : null,
|
||||
minLikeCount: minLikeCount,
|
||||
maxLikeCount: maxLikeCount,
|
||||
excludeBlocked: excludeBlocked,
|
||||
blockLevel: blockLevel,
|
||||
limit: limitEnabled ? int.tryParse(limit) : null,
|
||||
),
|
||||
orderBy: OrderBy(
|
||||
|
|
|
@ -268,18 +268,18 @@ mixin _$SmartListFormStore on _SmartListStore, Store {
|
|||
});
|
||||
}
|
||||
|
||||
final _$excludeBlockedAtom = Atom(name: '_SmartListStore.excludeBlocked');
|
||||
final _$blockLevelAtom = Atom(name: '_SmartListStore.blockLevel');
|
||||
|
||||
@override
|
||||
bool get excludeBlocked {
|
||||
_$excludeBlockedAtom.reportRead();
|
||||
return super.excludeBlocked;
|
||||
int get blockLevel {
|
||||
_$blockLevelAtom.reportRead();
|
||||
return super.blockLevel;
|
||||
}
|
||||
|
||||
@override
|
||||
set excludeBlocked(bool value) {
|
||||
_$excludeBlockedAtom.reportWrite(value, super.excludeBlocked, () {
|
||||
super.excludeBlocked = value;
|
||||
set blockLevel(int value) {
|
||||
_$blockLevelAtom.reportWrite(value, super.blockLevel, () {
|
||||
super.blockLevel = value;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -406,7 +406,7 @@ maxYearEnabled: ${maxYearEnabled},
|
|||
maxYear: ${maxYear},
|
||||
limitEnabled: ${limitEnabled},
|
||||
limit: ${limit},
|
||||
excludeBlocked: ${excludeBlocked},
|
||||
blockLevel: ${blockLevel},
|
||||
selectedArtists: ${selectedArtists},
|
||||
excludeArtists: ${excludeArtists},
|
||||
orderState: ${orderState}
|
||||
|
|
|
@ -2,6 +2,8 @@ import 'dart:io';
|
|||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'mucke_icons.dart';
|
||||
|
||||
ImageProvider getAlbumImage(String? albumArtPath) {
|
||||
// return Image.asset('assets/no_cover.png');
|
||||
|
||||
|
@ -35,3 +37,27 @@ String? validateNumber(bool enabled, String number) {
|
|||
if (!enabled) return null;
|
||||
return int.tryParse(number) == null ? 'Error' : null;
|
||||
}
|
||||
|
||||
IconData blockLevelIcon(int blockLevel) {
|
||||
switch (blockLevel) {
|
||||
case 1:
|
||||
return MuckeIcons.exclude_shuffle_all;
|
||||
case 2:
|
||||
return MuckeIcons.exclude_shuffle;
|
||||
case 3:
|
||||
return MuckeIcons.exclude_always;
|
||||
default:
|
||||
return MuckeIcons.exclude_never;
|
||||
}
|
||||
}
|
||||
|
||||
const _LIKE_COUNT_ICONS = [
|
||||
Icons.favorite_border_rounded,
|
||||
MuckeIcons.favorite_1_3,
|
||||
MuckeIcons.favorite_2_3,
|
||||
Icons.favorite_rounded
|
||||
];
|
||||
|
||||
IconData likeCountIcon(int likeCount) {
|
||||
return _LIKE_COUNT_ICONS[likeCount];
|
||||
}
|
||||
|
|
|
@ -1,58 +1,259 @@
|
|||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../../domain/entities/album.dart';
|
||||
import '../../domain/entities/song.dart';
|
||||
import '../utils.dart' as utils;
|
||||
import '../utils.dart';
|
||||
|
||||
class AlbumSliverAppBar extends StatelessWidget {
|
||||
class AlbumSliverAppBar extends StatefulWidget {
|
||||
const AlbumSliverAppBar({
|
||||
Key? key,
|
||||
required this.album,
|
||||
required this.songs,
|
||||
}) : super(key: key);
|
||||
|
||||
final Album album;
|
||||
final List<Song> songs;
|
||||
|
||||
@override
|
||||
State<AlbumSliverAppBar> createState() => _AlbumSliverAppBarState();
|
||||
}
|
||||
|
||||
class _AlbumSliverAppBarState extends State<AlbumSliverAppBar> {
|
||||
double get maxHeight => 220 + MediaQuery.of(context).padding.top;
|
||||
double get minHeight => kToolbarHeight + MediaQuery.of(context).padding.top;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverAppBar(
|
||||
systemOverlayStyle: SystemUiOverlayStyle.dark,
|
||||
pinned: true,
|
||||
expandedHeight: 250.0,
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
expandedHeight: maxHeight - MediaQuery.of(context).padding.top,
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0.0,
|
||||
iconTheme: const IconThemeData(
|
||||
color: Colors.white,
|
||||
),
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
centerTitle: true,
|
||||
titlePadding: const EdgeInsets.only(
|
||||
bottom: 16.0,
|
||||
left: 16.0,
|
||||
right: 16.0,
|
||||
),
|
||||
title: Text(
|
||||
album.title,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w300,
|
||||
fontSize: 16.0,
|
||||
color: Colors.white,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
background: Stack(
|
||||
flexibleSpace: Header(
|
||||
album: widget.album,
|
||||
songs: widget.songs,
|
||||
minHeight: minHeight,
|
||||
maxHeight: maxHeight,
|
||||
),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.chevron_left),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.more_vert),
|
||||
onPressed: () {},
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Header extends StatelessWidget {
|
||||
const Header({
|
||||
Key? key,
|
||||
required this.album,
|
||||
required this.songs,
|
||||
required this.maxHeight,
|
||||
required this.minHeight,
|
||||
}) : super(key: key);
|
||||
|
||||
final Album album;
|
||||
final List<Song> songs;
|
||||
final double maxHeight;
|
||||
final double minHeight;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final expandRatio = _calculateExpandRatio(constraints);
|
||||
final expandRatio2 = _calculateExpandRatio2(constraints);
|
||||
final animation = AlwaysStoppedAnimation(expandRatio);
|
||||
final animation2 = AlwaysStoppedAnimation(expandRatio2);
|
||||
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Image(
|
||||
image: utils.getAlbumImage(album.albumArtPath),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 2.0),
|
||||
child: _buildBackground(album, animation, maxHeight, minHeight),
|
||||
),
|
||||
_buildGradient(animation, context),
|
||||
_buildImage(album, animation, context),
|
||||
_buildTitle(album, songs, animation, animation2, context),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
double _calculateExpandRatio(BoxConstraints constraints) {
|
||||
var expandRatio = (constraints.maxHeight - minHeight) / (maxHeight - minHeight);
|
||||
if (expandRatio > 1.0) expandRatio = 1.0;
|
||||
if (expandRatio < 0.0) expandRatio = 0.0;
|
||||
return expandRatio;
|
||||
}
|
||||
|
||||
double _calculateExpandRatio2(BoxConstraints constraints) {
|
||||
var expandRatio = (constraints.maxHeight - minHeight) / (maxHeight - minHeight);
|
||||
if (expandRatio > 1.0)
|
||||
expandRatio = 1.0;
|
||||
else if (expandRatio < 0.8)
|
||||
expandRatio = 0.0;
|
||||
else
|
||||
expandRatio = (expandRatio - 0.8) * 5;
|
||||
return expandRatio;
|
||||
}
|
||||
|
||||
Align _buildTitle(
|
||||
Album album,
|
||||
List<Song> songs,
|
||||
Animation<double> animation,
|
||||
Animation<double> animation2,
|
||||
BuildContext context,
|
||||
) {
|
||||
final totalDuration =
|
||||
songs.fold(const Duration(milliseconds: 0), (Duration d, s) => d + s.duration);
|
||||
|
||||
return Align(
|
||||
alignment: AlignmentTween(
|
||||
begin: Alignment.center,
|
||||
end: Alignment.topLeft,
|
||||
).evaluate(animation),
|
||||
child: Container(
|
||||
margin: EdgeInsets.only(
|
||||
top: Tween<double>(
|
||||
begin: MediaQuery.of(context).padding.top,
|
||||
end: kToolbarHeight + MediaQuery.of(context).padding.top + 8,
|
||||
).evaluate(animation),
|
||||
left: Tween<double>(begin: 56, end: 152).evaluate(animation),
|
||||
right: Tween<double>(begin: 56, end: 16).evaluate(animation),
|
||||
bottom: Tween<double>(begin: 0, end: 16).evaluate(animation),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
album.title,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: Tween<double>(begin: 16, end: 24).evaluate(animation),
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.lerp(
|
||||
FontWeight.w300,
|
||||
FontWeight.w600,
|
||||
Tween<double>(begin: 0, end: 1).evaluate(animation),
|
||||
),
|
||||
|
||||
),
|
||||
),
|
||||
Container(
|
||||
color: Colors.black45,
|
||||
height: Tween<double>(begin: 0, end: 8).evaluate(animation),
|
||||
width: 10.0,
|
||||
),
|
||||
Text(
|
||||
album.artist,
|
||||
style: TextStyle(
|
||||
fontSize: Tween<double>(begin: 0, end: 16).evaluate(animation),
|
||||
color:
|
||||
Colors.white.withOpacity(Tween<double>(begin: 0, end: 1).evaluate(animation2)),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${album.pubYear.toString()} • ${songs.length} Songs • ${msToTimeString(totalDuration)}',
|
||||
style: TextStyle(
|
||||
fontSize: Tween<double>(begin: 0, end: 13).evaluate(animation),
|
||||
color:
|
||||
Colors.white.withOpacity(Tween<double>(begin: 0, end: 1).evaluate(animation2)),
|
||||
fontWeight: FontWeight.w300,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildImage(Album album, Animation<double> animation, BuildContext context) {
|
||||
return Positioned(
|
||||
width: 120,
|
||||
height: 120,
|
||||
left: 16,
|
||||
top: Tween<double>(
|
||||
begin: kToolbarHeight + MediaQuery.of(context).padding.top + 96,
|
||||
end: kToolbarHeight + MediaQuery.of(context).padding.top,
|
||||
).evaluate(animation),
|
||||
child: Container(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(2.0),
|
||||
boxShadow: const [
|
||||
BoxShadow(color: Colors.black26, blurRadius: 8, offset: Offset(0, 1)),
|
||||
],
|
||||
image: DecorationImage(
|
||||
image: utils.getAlbumImage(album.albumArtPath),
|
||||
fit: BoxFit.contain,
|
||||
opacity: Tween<double>(begin: 0, end: 1).evaluate(animation)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Container _buildGradient(Animation<double> animation, BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
ColorTween(
|
||||
begin: Theme.of(context).primaryColor,
|
||||
end: Theme.of(context).primaryColor.withOpacity(0.2),
|
||||
).evaluate(animation) ??
|
||||
Colors.transparent,
|
||||
ColorTween(
|
||||
begin: Theme.of(context).primaryColor,
|
||||
end: Theme.of(context).scaffoldBackgroundColor.withOpacity(0.4),
|
||||
).evaluate(animation) ??
|
||||
Colors.transparent,
|
||||
ColorTween(
|
||||
begin: Theme.of(context).primaryColor,
|
||||
end: Theme.of(context).scaffoldBackgroundColor,
|
||||
).evaluate(animation) ??
|
||||
Colors.transparent,
|
||||
],
|
||||
stops: const [0, 0.7, 1],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBackground(
|
||||
Album album,
|
||||
Animation<double> animation,
|
||||
double maxHeight,
|
||||
double minHeight,
|
||||
) {
|
||||
return ClipRect(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: ImageFiltered(
|
||||
imageFilter: ImageFilter.blur(sigmaX: 24.0, sigmaY: 24.0),
|
||||
child: Image(
|
||||
image: utils.getAlbumImage(album.albumArtPath),
|
||||
fit: BoxFit.cover,
|
||||
opacity: animation,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../../domain/entities/artist.dart';
|
||||
|
||||
|
@ -15,7 +14,6 @@ class ArtistHeader extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
const double height = 144.0;
|
||||
return SliverAppBar(
|
||||
systemOverlayStyle: SystemUiOverlayStyle.dark,
|
||||
pinned: true,
|
||||
expandedHeight: height,
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
|
|
42
lib/presentation/widgets/custom_modal_bottom_sheet.dart
Normal file
|
@ -0,0 +1,42 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../theming.dart';
|
||||
|
||||
class CustomModalBottomSheet {
|
||||
void call(BuildContext context, List<Widget> widgets) {
|
||||
final int count = 2 * widgets.length - 1;
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: DARK3,
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
boxShadow: const [
|
||||
BoxShadow(color: Colors.black26, blurRadius: 8, offset: Offset(0, 1)),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: List.generate(count, (index) {
|
||||
if (index.isEven) {
|
||||
return widgets[(index / 2).round()];
|
||||
} else {
|
||||
return Container(
|
||||
height: 1,
|
||||
color: Colors.white10,
|
||||
);
|
||||
}
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,12 +1,12 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import '../../constants.dart';
|
||||
|
||||
import '../../constants.dart';
|
||||
import '../../domain/entities/song.dart';
|
||||
import '../state/audio_store.dart';
|
||||
import '../state/music_data_store.dart';
|
||||
import '../theming.dart';
|
||||
import '../utils.dart';
|
||||
|
||||
class LikeButton extends StatelessWidget {
|
||||
const LikeButton({
|
||||
|
@ -28,7 +28,7 @@ class LikeButton extends StatelessWidget {
|
|||
if (song == null) {
|
||||
return IconButton(
|
||||
icon: Icon(
|
||||
Icons.favorite_rounded,
|
||||
likeCountIcon(0),
|
||||
size: iconSize,
|
||||
color: Colors.white24,
|
||||
),
|
||||
|
@ -37,47 +37,29 @@ class LikeButton extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
|
||||
if (song.likeCount == 0) {
|
||||
return IconButton(
|
||||
icon: Icon(
|
||||
Icons.favorite_rounded,
|
||||
size: iconSize,
|
||||
color: Colors.white24,
|
||||
),
|
||||
onPressed: () => musicDataStore.incrementLikeCount(song),
|
||||
visualDensity: VisualDensity.compact,
|
||||
);
|
||||
} else {
|
||||
return IconButton(
|
||||
icon: Stack(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.favorite_rounded,
|
||||
color: Theme.of(context).highlightColor,
|
||||
size: iconSize,
|
||||
),
|
||||
Text(
|
||||
song.likeCount.toString(),
|
||||
style: const TextStyle(
|
||||
color: DARK1,
|
||||
fontSize: 10.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
alignment: AlignmentDirectional.center,
|
||||
),
|
||||
onPressed: () {
|
||||
if (song.likeCount < MAX_LIKE_COUNT) {
|
||||
musicDataStore.incrementLikeCount(song);
|
||||
} else {
|
||||
musicDataStore.resetLikeCount(song);
|
||||
}
|
||||
},
|
||||
visualDensity: VisualDensity.compact,
|
||||
);
|
||||
}
|
||||
final likeCountColors = [
|
||||
Colors.white24,
|
||||
Colors.white54,
|
||||
Colors.white70,
|
||||
Theme.of(context).highlightColor,
|
||||
];
|
||||
|
||||
return IconButton(
|
||||
icon: Icon(
|
||||
likeCountIcon(song.likeCount),
|
||||
size: iconSize,
|
||||
color: likeCountColors[song.likeCount],
|
||||
),
|
||||
onPressed: () {
|
||||
if (song.likeCount < MAX_LIKE_COUNT) {
|
||||
musicDataStore.incrementLikeCount(song);
|
||||
} else {
|
||||
musicDataStore.resetLikeCount(song);
|
||||
}
|
||||
},
|
||||
visualDensity: VisualDensity.compact,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ import 'package:flutter_mobx/flutter_mobx.dart';
|
|||
import 'package:get_it/get_it.dart';
|
||||
|
||||
import '../../domain/entities/shuffle_mode.dart';
|
||||
import '../mucke_icons.dart';
|
||||
import '../state/audio_store.dart';
|
||||
|
||||
class ShuffleButton extends StatelessWidget {
|
||||
|
@ -41,7 +42,8 @@ class ShuffleButton extends StatelessWidget {
|
|||
case ShuffleMode.plus:
|
||||
return IconButton(
|
||||
icon: Icon(
|
||||
Icons.fingerprint_rounded,
|
||||
MuckeIcons.shuffle_heart,
|
||||
size: iconSize,
|
||||
color: Theme.of(context).highlightColor,
|
||||
),
|
||||
iconSize: iconSize,
|
||||
|
|
|
@ -7,6 +7,7 @@ import '../../domain/entities/song.dart';
|
|||
import '../state/audio_store.dart';
|
||||
import '../state/music_data_store.dart';
|
||||
import '../theming.dart';
|
||||
import 'custom_modal_bottom_sheet.dart';
|
||||
import 'song_info.dart';
|
||||
|
||||
class SongBottomSheet {
|
||||
|
@ -14,135 +15,121 @@ class SongBottomSheet {
|
|||
final AudioStore audioStore = GetIt.I<AudioStore>();
|
||||
final MusicDataStore musicDataStore = GetIt.I<MusicDataStore>();
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
backgroundColor: DARK2,
|
||||
builder: (context) {
|
||||
return Container(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
height: 2,
|
||||
color: LIGHT1,
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('Play next'),
|
||||
leading: const Icon(Icons.play_arrow_rounded),
|
||||
onTap: () {
|
||||
audioStore.playNext(song);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('Add to queue'),
|
||||
leading: const Icon(Icons.queue_rounded),
|
||||
onTap: () {
|
||||
audioStore.addToQueue(song);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('Add to playlist'),
|
||||
leading: const Icon(Icons.playlist_add_rounded),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return Observer(
|
||||
builder: (context) {
|
||||
final playlists = musicDataStore.playlistsStream.value ?? [];
|
||||
return SimpleDialog(
|
||||
backgroundColor: DARK3,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(HORIZONTAL_PADDING),
|
||||
child: Container(
|
||||
height: 300.0,
|
||||
width: 300.0,
|
||||
child: ListView.separated(
|
||||
itemCount: playlists.length,
|
||||
itemBuilder: (_, int index) {
|
||||
final Playlist playlist = playlists[index];
|
||||
return ListTile(
|
||||
title: Text(playlist.name),
|
||||
onTap: () {
|
||||
musicDataStore.addSongToPlaylist(playlist, song);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
);
|
||||
},
|
||||
separatorBuilder: (BuildContext context, int index) =>
|
||||
const SizedBox(
|
||||
height: 4.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
CustomModalBottomSheet()(
|
||||
context,
|
||||
[
|
||||
ListTile(
|
||||
title: const Text('Play next'),
|
||||
leading: const Icon(Icons.play_arrow_rounded),
|
||||
onTap: () {
|
||||
audioStore.playNext(song);
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('Add to queue'),
|
||||
leading: const Icon(Icons.queue_rounded),
|
||||
onTap: () {
|
||||
audioStore.addToQueue(song);
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('Add to playlist'),
|
||||
leading: const Icon(Icons.playlist_add_rounded),
|
||||
onTap: () {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return Observer(
|
||||
builder: (context) {
|
||||
final playlists = musicDataStore.playlistsStream.value ?? [];
|
||||
return SimpleDialog(
|
||||
backgroundColor: DARK3,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(HORIZONTAL_PADDING),
|
||||
child: Container(
|
||||
height: 300.0,
|
||||
width: 300.0,
|
||||
child: ListView.separated(
|
||||
itemCount: playlists.length,
|
||||
itemBuilder: (_, int index) {
|
||||
final Playlist playlist = playlists[index];
|
||||
return ListTile(
|
||||
title: Text(playlist.name),
|
||||
onTap: () {
|
||||
musicDataStore.addSongToPlaylist(playlist, song);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
);
|
||||
},
|
||||
separatorBuilder: (BuildContext context, int index) => const SizedBox(
|
||||
height: 4.0,
|
||||
),
|
||||
SimpleDialogOption(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text(
|
||||
'Cancel',
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
// TODO: adapt to multiple block levels
|
||||
// ListTile(
|
||||
// title: song.blocked ? const Text('Unblock song') : const Text('Block song'),
|
||||
// leading: song.blocked
|
||||
// ? const Icon(Icons.check_circle_outline_rounded)
|
||||
// : const Icon(Icons.remove_circle_outline_rounded),
|
||||
// onTap: () {
|
||||
// musicDataStore.setSongBlocked(song, !song.blocked);
|
||||
// Navigator.pop(context);
|
||||
// },
|
||||
// ),
|
||||
ListTile(
|
||||
title: const Text('Show song info'),
|
||||
leading: const Icon(Icons.info),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return SimpleDialog(
|
||||
backgroundColor: DARK3,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(HORIZONTAL_PADDING),
|
||||
child: SongInfo(song),
|
||||
),
|
||||
SimpleDialogOption(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text(
|
||||
'Close',
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
SimpleDialogOption(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text(
|
||||
'Cancel',
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
// TODO: adapt to multiple block levels
|
||||
// ListTile(
|
||||
// title: song.blocked ? const Text('Unblock song') : const Text('Block song'),
|
||||
// leading: song.blocked
|
||||
// ? const Icon(Icons.check_circle_outline_rounded)
|
||||
// : const Icon(Icons.remove_circle_outline_rounded),
|
||||
// onTap: () {
|
||||
// musicDataStore.setSongBlocked(song, !song.blocked);
|
||||
// Navigator.of(context, rootNavigator: true).pop();
|
||||
// },
|
||||
// ),
|
||||
ListTile(
|
||||
title: const Text('Show song info'),
|
||||
leading: const Icon(Icons.info),
|
||||
onTap: () {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return SimpleDialog(
|
||||
backgroundColor: DARK3,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(HORIZONTAL_PADDING),
|
||||
child: SongInfo(song),
|
||||
),
|
||||
SimpleDialogOption(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text(
|
||||
'Close',
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,8 @@ import '../../domain/entities/song.dart';
|
|||
import '../state/audio_store.dart';
|
||||
import '../state/music_data_store.dart';
|
||||
import '../theming.dart';
|
||||
import '../utils.dart';
|
||||
import 'custom_modal_bottom_sheet.dart';
|
||||
import 'like_button.dart';
|
||||
|
||||
class SongCustomizationButtons extends StatelessWidget {
|
||||
|
@ -13,7 +15,6 @@ class SongCustomizationButtons extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final MusicDataStore musicDataStore = GetIt.I<MusicDataStore>();
|
||||
final AudioStore audioStore = GetIt.I<AudioStore>();
|
||||
|
||||
return Observer(
|
||||
|
@ -41,7 +42,7 @@ class SongCustomizationButtons extends StatelessWidget {
|
|||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
_blockIcon(song.blockLevel),
|
||||
blockLevelIcon(song.blockLevel),
|
||||
size: 20.0,
|
||||
color: song.blockLevel == 0 ? Colors.white24 : Colors.white,
|
||||
),
|
||||
|
@ -71,61 +72,35 @@ class SongCustomizationButtons extends StatelessWidget {
|
|||
final MusicDataStore musicDataStore = GetIt.I<MusicDataStore>();
|
||||
final AudioStore audioStore = GetIt.I<AudioStore>();
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
backgroundColor: DARK2,
|
||||
builder: (context) {
|
||||
return Container(
|
||||
child: Observer(
|
||||
builder: (BuildContext context) {
|
||||
final song = audioStore.currentSongStream.value;
|
||||
if (song == null) {
|
||||
return Container();
|
||||
}
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
height: 2,
|
||||
color: LIGHT1,
|
||||
),
|
||||
SwitchListTile(
|
||||
title: const Text('Always play previous song before'),
|
||||
value: song.previous != '',
|
||||
onChanged: (bool value) {
|
||||
musicDataStore.togglePreviousSongLink(song);
|
||||
},
|
||||
),
|
||||
SwitchListTile(
|
||||
title: const Text('Always play next song after'),
|
||||
value: song.next != '',
|
||||
onChanged: (bool value) {
|
||||
musicDataStore.toggleNextSongLink(song);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
CustomModalBottomSheet()(
|
||||
context,
|
||||
[
|
||||
Observer(builder: (context) {
|
||||
final song = audioStore.currentSongStream.value;
|
||||
if (song == null) return Container();
|
||||
return SwitchListTile(
|
||||
title: const Text('Always play previous song before'),
|
||||
value: song.previous != '',
|
||||
onChanged: (bool value) {
|
||||
musicDataStore.togglePreviousSongLink(song);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
Observer(builder: (context) {
|
||||
final song = audioStore.currentSongStream.value;
|
||||
if (song == null) return Container();
|
||||
return SwitchListTile(
|
||||
title: const Text('Always play next song after'),
|
||||
value: song.next != '',
|
||||
onChanged: (bool value) {
|
||||
musicDataStore.toggleNextSongLink(song);
|
||||
},
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
IconData _blockIcon(int blockLevel) {
|
||||
switch (blockLevel) {
|
||||
case 1:
|
||||
return Icons.sentiment_neutral_rounded;
|
||||
case 2:
|
||||
return Icons.sentiment_dissatisfied_rounded;
|
||||
case 3:
|
||||
return Icons.sentiment_very_dissatisfied_rounded;
|
||||
default:
|
||||
return Icons.sentiment_satisfied_rounded;
|
||||
}
|
||||
}
|
||||
|
||||
void _editBlockLevel(BuildContext context) {
|
||||
final MusicDataStore musicDataStore = GetIt.I<MusicDataStore>();
|
||||
final AudioStore audioStore = GetIt.I<AudioStore>();
|
||||
|
@ -133,87 +108,39 @@ class SongCustomizationButtons extends StatelessWidget {
|
|||
const TextStyle _active = TextStyle(color: Colors.white);
|
||||
const TextStyle _inactive = TextStyle(color: Colors.white54);
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
backgroundColor: DARK2,
|
||||
builder: (context) {
|
||||
return Container(
|
||||
child: Observer(
|
||||
builder: (BuildContext context) {
|
||||
final song = audioStore.currentSongStream.value;
|
||||
if (song == null) {
|
||||
return Container();
|
||||
}
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
height: 2,
|
||||
color: LIGHT1,
|
||||
),
|
||||
ListTile(
|
||||
title: Text(
|
||||
"Don't exclude this song.",
|
||||
style: song.blockLevel == 0 ? _active : _inactive,
|
||||
),
|
||||
leading: Icon(
|
||||
Icons.sentiment_satisfied_rounded,
|
||||
color: song.blockLevel == 0 ? LIGHT1 : Colors.white54,
|
||||
),
|
||||
enabled: song.blockLevel != 0,
|
||||
onTap: () {
|
||||
musicDataStore.setSongBlocked(song, 0);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(
|
||||
'Exclude when shuffling all songs.',
|
||||
style: song.blockLevel == 1 ? _active : _inactive,
|
||||
),
|
||||
leading: Icon(
|
||||
Icons.sentiment_neutral_rounded,
|
||||
color: song.blockLevel == 1 ? LIGHT1 : Colors.white54,
|
||||
),
|
||||
enabled: song.blockLevel != 1,
|
||||
onTap: () {
|
||||
musicDataStore.setSongBlocked(song, 1);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(
|
||||
'Exclude when shuffling.',
|
||||
style: song.blockLevel == 2 ? _active : _inactive,
|
||||
),
|
||||
leading: Icon(
|
||||
Icons.sentiment_dissatisfied_rounded,
|
||||
color: song.blockLevel == 2 ? LIGHT1 : Colors.white54,
|
||||
),
|
||||
enabled: song.blockLevel != 2,
|
||||
onTap: () {
|
||||
musicDataStore.setSongBlocked(song, 2);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(
|
||||
'Always exclude this song.',
|
||||
style: song.blockLevel == 3 ? _active : _inactive,
|
||||
),
|
||||
leading: Icon(
|
||||
Icons.sentiment_very_dissatisfied_rounded,
|
||||
color: song.blockLevel == 3 ? LIGHT1 : Colors.white54,
|
||||
),
|
||||
enabled: song.blockLevel != 3,
|
||||
onTap: () {
|
||||
musicDataStore.setSongBlocked(song, 3);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
const descriptions = <String>[
|
||||
"Don't exclude this song.",
|
||||
'Exclude when shuffling all songs.',
|
||||
'Exclude when shuffling.',
|
||||
'Always exclude this song.'
|
||||
];
|
||||
|
||||
CustomModalBottomSheet()(
|
||||
context,
|
||||
List.generate(
|
||||
descriptions.length,
|
||||
(index) => Observer(
|
||||
builder: (BuildContext context) {
|
||||
final song = audioStore.currentSongStream.value;
|
||||
if (song == null) return Container();
|
||||
return ListTile(
|
||||
title: Text(
|
||||
descriptions[index],
|
||||
style: song.blockLevel == index ? _active : _inactive,
|
||||
),
|
||||
leading: Icon(
|
||||
blockLevelIcon(index),
|
||||
size: 24.0,
|
||||
color: song.blockLevel == index ? LIGHT1 : Colors.white54,
|
||||
),
|
||||
enabled: song.blockLevel != index,
|
||||
onTap: () {
|
||||
musicDataStore.setSongBlocked(song, index);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
|||
import '../../domain/entities/song.dart';
|
||||
import '../theming.dart';
|
||||
import '../utils.dart' as utils;
|
||||
import '../utils.dart';
|
||||
|
||||
enum Subtitle { artist, artistAlbum, stats }
|
||||
|
||||
|
@ -15,8 +16,6 @@ class SongListTile extends StatelessWidget {
|
|||
this.highlight = false,
|
||||
this.showAlbum = true,
|
||||
this.subtitle = Subtitle.artist,
|
||||
this.isBlockSkippedSongsEnabled,
|
||||
this.blockSkippedSongsThreshold,
|
||||
}) : super(key: key);
|
||||
|
||||
final Song song;
|
||||
|
@ -25,14 +24,9 @@ class SongListTile extends StatelessWidget {
|
|||
final bool highlight;
|
||||
final bool showAlbum;
|
||||
final Subtitle subtitle;
|
||||
final bool? isBlockSkippedSongsEnabled;
|
||||
final int? blockSkippedSongsThreshold;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isBlockEnabled = isBlockSkippedSongsEnabled ?? false;
|
||||
final blockThreshold = blockSkippedSongsThreshold ?? 1000;
|
||||
|
||||
final Widget leading = showAlbum
|
||||
? Image(
|
||||
image: utils.getAlbumImage(song.albumArtPath),
|
||||
|
@ -103,24 +97,24 @@ class SongListTile extends StatelessWidget {
|
|||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (song.blockLevel == 1)
|
||||
const Icon(
|
||||
Icons.sentiment_neutral_rounded,
|
||||
size: 14.0,
|
||||
color: Colors.white38,
|
||||
),
|
||||
if (song.blockLevel == 2)
|
||||
const Icon(
|
||||
Icons.sentiment_dissatisfied_rounded,
|
||||
size: 14.0,
|
||||
color: Colors.white38,
|
||||
),
|
||||
if (song.blockLevel == 3)
|
||||
const Icon(
|
||||
Icons.sentiment_very_dissatisfied_rounded,
|
||||
size: 14.0,
|
||||
color: Colors.white38,
|
||||
if (song.blockLevel > 0)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 4.0),
|
||||
child: Icon(
|
||||
blockLevelIcon(song.blockLevel),
|
||||
size: 16.0,
|
||||
color: Colors.white38,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
likeCountIcon(song.likeCount),
|
||||
size: 16.0,
|
||||
color: song.likeCount == 3
|
||||
? LIGHT2
|
||||
: Colors.white.withOpacity(
|
||||
0.2 + 0.18 * song.likeCount,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.more_vert),
|
||||
iconSize: 20.0,
|
||||
|
|
|
@ -42,7 +42,11 @@ class LocalMusicFetcherImpl implements LocalMusicFetcher {
|
|||
final Set<String> artistSet = {};
|
||||
|
||||
final Directory dir = await getApplicationSupportDirectory();
|
||||
int count = 1;
|
||||
for (final file in files.toSet()) {
|
||||
if (count % 10 == 0) _log.d('Files scanned: $count');
|
||||
count++;
|
||||
|
||||
final tags = await _audiotagger.readTags(path: file.path);
|
||||
final audioFile = await _audiotagger.readAudioFile(path: file.path);
|
||||
|
||||
|
|
|
@ -162,7 +162,7 @@ class PlaylistDao extends DatabaseAccessor<MoorDatabase>
|
|||
maxLikeCount: Value(filter.maxLikeCount),
|
||||
minYear: Value(filter.minYear),
|
||||
maxYear: Value(filter.maxYear),
|
||||
excludeBlocked: Value(filter.excludeBlocked),
|
||||
blockLevel: Value(filter.blockLevel),
|
||||
limit: Value(filter.limit),
|
||||
orderCriteria: Value(orderCriteria),
|
||||
orderDirections: Value(orderDirections),
|
||||
|
@ -269,7 +269,7 @@ class PlaylistDao extends DatabaseAccessor<MoorDatabase>
|
|||
query = query..where((tbl) => tbl.year.isSmallerOrEqualValue(filter.maxYear));
|
||||
|
||||
// TODO: adapt for different block levels
|
||||
if (filter.excludeBlocked) query = query..where((tbl) => tbl.blockLevel.isSmallerThanValue(1));
|
||||
query = query..where((tbl) => tbl.blockLevel.isSmallerOrEqualValue(filter.blockLevel));
|
||||
|
||||
if (filter.artists.isNotEmpty) {
|
||||
if (filter.excludeArtists)
|
||||
|
|
|
@ -5,11 +5,11 @@ import 'dart:isolate';
|
|||
import 'package:moor/ffi.dart';
|
||||
import 'package:moor/isolate.dart';
|
||||
import 'package:moor/moor.dart';
|
||||
import 'package:mucke/domain/entities/playable.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import '../../constants.dart';
|
||||
import '../../domain/entities/playable.dart';
|
||||
import 'moor/music_data_dao.dart';
|
||||
import 'moor/persistent_state_dao.dart';
|
||||
import 'moor/playlist_dao.dart';
|
||||
|
@ -118,7 +118,7 @@ class SmartLists extends Table {
|
|||
|
||||
// Filter
|
||||
BoolColumn get excludeArtists => boolean().withDefault(const Constant(false))();
|
||||
BoolColumn get excludeBlocked => boolean().withDefault(const Constant(false))();
|
||||
IntColumn get blockLevel => integer().withDefault(const Constant(0))();
|
||||
IntColumn get minLikeCount => integer().withDefault(const Constant(0))();
|
||||
IntColumn get maxLikeCount => integer().withDefault(const Constant(5))();
|
||||
IntColumn get minPlayCount => integer().nullable()();
|
||||
|
@ -186,7 +186,7 @@ class MoorDatabase extends _$MoorDatabase {
|
|||
MoorDatabase.connect(DatabaseConnection connection) : super.connect(connection);
|
||||
|
||||
@override
|
||||
int get schemaVersion => 6;
|
||||
int get schemaVersion => 2;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration => MigrationStrategy(beforeOpen: (details) async {
|
||||
|
@ -213,47 +213,9 @@ class MoorDatabase extends _$MoorDatabase {
|
|||
}
|
||||
}, onUpgrade: (Migrator m, int from, int to) async {
|
||||
print('$from -> $to');
|
||||
if (from == 1) {
|
||||
await m.createTable(smartLists);
|
||||
await m.createTable(smartListArtists);
|
||||
|
||||
await m.addColumn(songs, songs.timeAdded);
|
||||
await m.addColumn(songs, songs.year);
|
||||
await m.alterTable(TableMigration(songs));
|
||||
|
||||
final albumMap =
|
||||
await select(albums).get().then((value) => {for (var a in value) a.id: a.year});
|
||||
|
||||
await transaction(() async {
|
||||
for (final album in albumMap.entries) {
|
||||
await (update(songs)..where((tbl) => tbl.albumId.equals(album.key)))
|
||||
.write(SongsCompanion(year: Value(album.value)));
|
||||
}
|
||||
});
|
||||
}
|
||||
if (from < 3) {
|
||||
await m.alterTable(
|
||||
TableMigration(songs, columnTransformer: {
|
||||
songs.timeAdded: currentDateAndTime,
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (from < 4) {
|
||||
if (from < 2) {
|
||||
await m.addColumn(smartLists, smartLists.blockLevel);
|
||||
await m.alterTable(TableMigration(smartLists));
|
||||
await m.createTable(playlists);
|
||||
await m.createTable(playlistEntries);
|
||||
}
|
||||
if (from < 5) {
|
||||
await m.addColumn(smartLists, smartLists.minSkipCount);
|
||||
await m.addColumn(smartLists, smartLists.maxSkipCount);
|
||||
}
|
||||
if (from < 6) {
|
||||
await (update(songs)..where((tbl) => tbl.likeCount.equals(3)))
|
||||
.write(const SongsCompanion(likeCount: Value(2)));
|
||||
await (update(songs)..where((tbl) => tbl.likeCount.equals(4)))
|
||||
.write(const SongsCompanion(likeCount: Value(3)));
|
||||
await (update(songs)..where((tbl) => tbl.likeCount.equals(5)))
|
||||
.write(const SongsCompanion(likeCount: Value(3)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -2360,7 +2360,7 @@ class MoorSmartList extends DataClass implements Insertable<MoorSmartList> {
|
|||
final String name;
|
||||
final String? shuffleMode;
|
||||
final bool excludeArtists;
|
||||
final bool excludeBlocked;
|
||||
final int blockLevel;
|
||||
final int minLikeCount;
|
||||
final int maxLikeCount;
|
||||
final int? minPlayCount;
|
||||
|
@ -2377,7 +2377,7 @@ class MoorSmartList extends DataClass implements Insertable<MoorSmartList> {
|
|||
required this.name,
|
||||
this.shuffleMode,
|
||||
required this.excludeArtists,
|
||||
required this.excludeBlocked,
|
||||
required this.blockLevel,
|
||||
required this.minLikeCount,
|
||||
required this.maxLikeCount,
|
||||
this.minPlayCount,
|
||||
|
@ -2402,8 +2402,8 @@ class MoorSmartList extends DataClass implements Insertable<MoorSmartList> {
|
|||
.mapFromDatabaseResponse(data['${effectivePrefix}shuffle_mode']),
|
||||
excludeArtists: const BoolType()
|
||||
.mapFromDatabaseResponse(data['${effectivePrefix}exclude_artists'])!,
|
||||
excludeBlocked: const BoolType()
|
||||
.mapFromDatabaseResponse(data['${effectivePrefix}exclude_blocked'])!,
|
||||
blockLevel: const IntType()
|
||||
.mapFromDatabaseResponse(data['${effectivePrefix}block_level'])!,
|
||||
minLikeCount: const IntType()
|
||||
.mapFromDatabaseResponse(data['${effectivePrefix}min_like_count'])!,
|
||||
maxLikeCount: const IntType()
|
||||
|
@ -2437,7 +2437,7 @@ class MoorSmartList extends DataClass implements Insertable<MoorSmartList> {
|
|||
map['shuffle_mode'] = Variable<String?>(shuffleMode);
|
||||
}
|
||||
map['exclude_artists'] = Variable<bool>(excludeArtists);
|
||||
map['exclude_blocked'] = Variable<bool>(excludeBlocked);
|
||||
map['block_level'] = Variable<int>(blockLevel);
|
||||
map['min_like_count'] = Variable<int>(minLikeCount);
|
||||
map['max_like_count'] = Variable<int>(maxLikeCount);
|
||||
if (!nullToAbsent || minPlayCount != null) {
|
||||
|
@ -2474,7 +2474,7 @@ class MoorSmartList extends DataClass implements Insertable<MoorSmartList> {
|
|||
? const Value.absent()
|
||||
: Value(shuffleMode),
|
||||
excludeArtists: Value(excludeArtists),
|
||||
excludeBlocked: Value(excludeBlocked),
|
||||
blockLevel: Value(blockLevel),
|
||||
minLikeCount: Value(minLikeCount),
|
||||
maxLikeCount: Value(maxLikeCount),
|
||||
minPlayCount: minPlayCount == null && nullToAbsent
|
||||
|
@ -2510,7 +2510,7 @@ class MoorSmartList extends DataClass implements Insertable<MoorSmartList> {
|
|||
name: serializer.fromJson<String>(json['name']),
|
||||
shuffleMode: serializer.fromJson<String?>(json['shuffleMode']),
|
||||
excludeArtists: serializer.fromJson<bool>(json['excludeArtists']),
|
||||
excludeBlocked: serializer.fromJson<bool>(json['excludeBlocked']),
|
||||
blockLevel: serializer.fromJson<int>(json['blockLevel']),
|
||||
minLikeCount: serializer.fromJson<int>(json['minLikeCount']),
|
||||
maxLikeCount: serializer.fromJson<int>(json['maxLikeCount']),
|
||||
minPlayCount: serializer.fromJson<int?>(json['minPlayCount']),
|
||||
|
@ -2532,7 +2532,7 @@ class MoorSmartList extends DataClass implements Insertable<MoorSmartList> {
|
|||
'name': serializer.toJson<String>(name),
|
||||
'shuffleMode': serializer.toJson<String?>(shuffleMode),
|
||||
'excludeArtists': serializer.toJson<bool>(excludeArtists),
|
||||
'excludeBlocked': serializer.toJson<bool>(excludeBlocked),
|
||||
'blockLevel': serializer.toJson<int>(blockLevel),
|
||||
'minLikeCount': serializer.toJson<int>(minLikeCount),
|
||||
'maxLikeCount': serializer.toJson<int>(maxLikeCount),
|
||||
'minPlayCount': serializer.toJson<int?>(minPlayCount),
|
||||
|
@ -2552,7 +2552,7 @@ class MoorSmartList extends DataClass implements Insertable<MoorSmartList> {
|
|||
String? name,
|
||||
String? shuffleMode,
|
||||
bool? excludeArtists,
|
||||
bool? excludeBlocked,
|
||||
int? blockLevel,
|
||||
int? minLikeCount,
|
||||
int? maxLikeCount,
|
||||
int? minPlayCount,
|
||||
|
@ -2569,7 +2569,7 @@ class MoorSmartList extends DataClass implements Insertable<MoorSmartList> {
|
|||
name: name ?? this.name,
|
||||
shuffleMode: shuffleMode ?? this.shuffleMode,
|
||||
excludeArtists: excludeArtists ?? this.excludeArtists,
|
||||
excludeBlocked: excludeBlocked ?? this.excludeBlocked,
|
||||
blockLevel: blockLevel ?? this.blockLevel,
|
||||
minLikeCount: minLikeCount ?? this.minLikeCount,
|
||||
maxLikeCount: maxLikeCount ?? this.maxLikeCount,
|
||||
minPlayCount: minPlayCount ?? this.minPlayCount,
|
||||
|
@ -2589,7 +2589,7 @@ class MoorSmartList extends DataClass implements Insertable<MoorSmartList> {
|
|||
..write('name: $name, ')
|
||||
..write('shuffleMode: $shuffleMode, ')
|
||||
..write('excludeArtists: $excludeArtists, ')
|
||||
..write('excludeBlocked: $excludeBlocked, ')
|
||||
..write('blockLevel: $blockLevel, ')
|
||||
..write('minLikeCount: $minLikeCount, ')
|
||||
..write('maxLikeCount: $maxLikeCount, ')
|
||||
..write('minPlayCount: $minPlayCount, ')
|
||||
|
@ -2615,7 +2615,7 @@ class MoorSmartList extends DataClass implements Insertable<MoorSmartList> {
|
|||
$mrjc(
|
||||
excludeArtists.hashCode,
|
||||
$mrjc(
|
||||
excludeBlocked.hashCode,
|
||||
blockLevel.hashCode,
|
||||
$mrjc(
|
||||
minLikeCount.hashCode,
|
||||
$mrjc(
|
||||
|
@ -2647,7 +2647,7 @@ class MoorSmartList extends DataClass implements Insertable<MoorSmartList> {
|
|||
other.name == this.name &&
|
||||
other.shuffleMode == this.shuffleMode &&
|
||||
other.excludeArtists == this.excludeArtists &&
|
||||
other.excludeBlocked == this.excludeBlocked &&
|
||||
other.blockLevel == this.blockLevel &&
|
||||
other.minLikeCount == this.minLikeCount &&
|
||||
other.maxLikeCount == this.maxLikeCount &&
|
||||
other.minPlayCount == this.minPlayCount &&
|
||||
|
@ -2666,7 +2666,7 @@ class SmartListsCompanion extends UpdateCompanion<MoorSmartList> {
|
|||
final Value<String> name;
|
||||
final Value<String?> shuffleMode;
|
||||
final Value<bool> excludeArtists;
|
||||
final Value<bool> excludeBlocked;
|
||||
final Value<int> blockLevel;
|
||||
final Value<int> minLikeCount;
|
||||
final Value<int> maxLikeCount;
|
||||
final Value<int?> minPlayCount;
|
||||
|
@ -2683,7 +2683,7 @@ class SmartListsCompanion extends UpdateCompanion<MoorSmartList> {
|
|||
this.name = const Value.absent(),
|
||||
this.shuffleMode = const Value.absent(),
|
||||
this.excludeArtists = const Value.absent(),
|
||||
this.excludeBlocked = const Value.absent(),
|
||||
this.blockLevel = const Value.absent(),
|
||||
this.minLikeCount = const Value.absent(),
|
||||
this.maxLikeCount = const Value.absent(),
|
||||
this.minPlayCount = const Value.absent(),
|
||||
|
@ -2701,7 +2701,7 @@ class SmartListsCompanion extends UpdateCompanion<MoorSmartList> {
|
|||
required String name,
|
||||
this.shuffleMode = const Value.absent(),
|
||||
this.excludeArtists = const Value.absent(),
|
||||
this.excludeBlocked = const Value.absent(),
|
||||
this.blockLevel = const Value.absent(),
|
||||
this.minLikeCount = const Value.absent(),
|
||||
this.maxLikeCount = const Value.absent(),
|
||||
this.minPlayCount = const Value.absent(),
|
||||
|
@ -2721,7 +2721,7 @@ class SmartListsCompanion extends UpdateCompanion<MoorSmartList> {
|
|||
Expression<String>? name,
|
||||
Expression<String?>? shuffleMode,
|
||||
Expression<bool>? excludeArtists,
|
||||
Expression<bool>? excludeBlocked,
|
||||
Expression<int>? blockLevel,
|
||||
Expression<int>? minLikeCount,
|
||||
Expression<int>? maxLikeCount,
|
||||
Expression<int?>? minPlayCount,
|
||||
|
@ -2739,7 +2739,7 @@ class SmartListsCompanion extends UpdateCompanion<MoorSmartList> {
|
|||
if (name != null) 'name': name,
|
||||
if (shuffleMode != null) 'shuffle_mode': shuffleMode,
|
||||
if (excludeArtists != null) 'exclude_artists': excludeArtists,
|
||||
if (excludeBlocked != null) 'exclude_blocked': excludeBlocked,
|
||||
if (blockLevel != null) 'block_level': blockLevel,
|
||||
if (minLikeCount != null) 'min_like_count': minLikeCount,
|
||||
if (maxLikeCount != null) 'max_like_count': maxLikeCount,
|
||||
if (minPlayCount != null) 'min_play_count': minPlayCount,
|
||||
|
@ -2759,7 +2759,7 @@ class SmartListsCompanion extends UpdateCompanion<MoorSmartList> {
|
|||
Value<String>? name,
|
||||
Value<String?>? shuffleMode,
|
||||
Value<bool>? excludeArtists,
|
||||
Value<bool>? excludeBlocked,
|
||||
Value<int>? blockLevel,
|
||||
Value<int>? minLikeCount,
|
||||
Value<int>? maxLikeCount,
|
||||
Value<int?>? minPlayCount,
|
||||
|
@ -2776,7 +2776,7 @@ class SmartListsCompanion extends UpdateCompanion<MoorSmartList> {
|
|||
name: name ?? this.name,
|
||||
shuffleMode: shuffleMode ?? this.shuffleMode,
|
||||
excludeArtists: excludeArtists ?? this.excludeArtists,
|
||||
excludeBlocked: excludeBlocked ?? this.excludeBlocked,
|
||||
blockLevel: blockLevel ?? this.blockLevel,
|
||||
minLikeCount: minLikeCount ?? this.minLikeCount,
|
||||
maxLikeCount: maxLikeCount ?? this.maxLikeCount,
|
||||
minPlayCount: minPlayCount ?? this.minPlayCount,
|
||||
|
@ -2806,8 +2806,8 @@ class SmartListsCompanion extends UpdateCompanion<MoorSmartList> {
|
|||
if (excludeArtists.present) {
|
||||
map['exclude_artists'] = Variable<bool>(excludeArtists.value);
|
||||
}
|
||||
if (excludeBlocked.present) {
|
||||
map['exclude_blocked'] = Variable<bool>(excludeBlocked.value);
|
||||
if (blockLevel.present) {
|
||||
map['block_level'] = Variable<int>(blockLevel.value);
|
||||
}
|
||||
if (minLikeCount.present) {
|
||||
map['min_like_count'] = Variable<int>(minLikeCount.value);
|
||||
|
@ -2852,7 +2852,7 @@ class SmartListsCompanion extends UpdateCompanion<MoorSmartList> {
|
|||
..write('name: $name, ')
|
||||
..write('shuffleMode: $shuffleMode, ')
|
||||
..write('excludeArtists: $excludeArtists, ')
|
||||
..write('excludeBlocked: $excludeBlocked, ')
|
||||
..write('blockLevel: $blockLevel, ')
|
||||
..write('minLikeCount: $minLikeCount, ')
|
||||
..write('maxLikeCount: $maxLikeCount, ')
|
||||
..write('minPlayCount: $minPlayCount, ')
|
||||
|
@ -2914,13 +2914,12 @@ class $SmartListsTable extends SmartLists
|
|||
defaultValue: const Constant(false));
|
||||
}
|
||||
|
||||
final VerificationMeta _excludeBlockedMeta =
|
||||
const VerificationMeta('excludeBlocked');
|
||||
final VerificationMeta _blockLevelMeta = const VerificationMeta('blockLevel');
|
||||
@override
|
||||
late final GeneratedBoolColumn excludeBlocked = _constructExcludeBlocked();
|
||||
GeneratedBoolColumn _constructExcludeBlocked() {
|
||||
return GeneratedBoolColumn('exclude_blocked', $tableName, false,
|
||||
defaultValue: const Constant(false));
|
||||
late final GeneratedIntColumn blockLevel = _constructBlockLevel();
|
||||
GeneratedIntColumn _constructBlockLevel() {
|
||||
return GeneratedIntColumn('block_level', $tableName, false,
|
||||
defaultValue: const Constant(0));
|
||||
}
|
||||
|
||||
final VerificationMeta _minLikeCountMeta =
|
||||
|
@ -3052,7 +3051,7 @@ class $SmartListsTable extends SmartLists
|
|||
name,
|
||||
shuffleMode,
|
||||
excludeArtists,
|
||||
excludeBlocked,
|
||||
blockLevel,
|
||||
minLikeCount,
|
||||
maxLikeCount,
|
||||
minPlayCount,
|
||||
|
@ -3097,11 +3096,11 @@ class $SmartListsTable extends SmartLists
|
|||
excludeArtists.isAcceptableOrUnknown(
|
||||
data['exclude_artists']!, _excludeArtistsMeta));
|
||||
}
|
||||
if (data.containsKey('exclude_blocked')) {
|
||||
if (data.containsKey('block_level')) {
|
||||
context.handle(
|
||||
_excludeBlockedMeta,
|
||||
excludeBlocked.isAcceptableOrUnknown(
|
||||
data['exclude_blocked']!, _excludeBlockedMeta));
|
||||
_blockLevelMeta,
|
||||
blockLevel.isAcceptableOrUnknown(
|
||||
data['block_level']!, _blockLevelMeta));
|
||||
}
|
||||
if (data.containsKey('min_like_count')) {
|
||||
context.handle(
|
||||
|
|
|
@ -32,7 +32,7 @@ class SmartListModel extends SmartList {
|
|||
maxSkipCount: moorSmartList.maxSkipCount,
|
||||
minYear: moorSmartList.minYear,
|
||||
maxYear: moorSmartList.maxYear,
|
||||
excludeBlocked: moorSmartList.excludeBlocked,
|
||||
blockLevel: moorSmartList.blockLevel,
|
||||
limit: moorSmartList.limit,
|
||||
);
|
||||
|
||||
|
@ -63,7 +63,7 @@ class SmartListModel extends SmartList {
|
|||
maxSkipCount: m.Value(filter.maxSkipCount),
|
||||
minYear: m.Value(filter.minYear),
|
||||
maxYear: m.Value(filter.maxYear),
|
||||
excludeBlocked: m.Value(filter.excludeBlocked),
|
||||
blockLevel: m.Value(filter.blockLevel),
|
||||
limit: m.Value(filter.limit),
|
||||
orderCriteria: m.Value(orderBy.orderCriteria.join(',')),
|
||||
orderDirections: m.Value(orderBy.orderDirections.join(',')),
|
||||
|
|
|
@ -86,9 +86,17 @@ class MusicDataRepositoryImpl implements MusicDataRepository {
|
|||
|
||||
final localMusic = await _localMusicFetcher.getLocalMusic();
|
||||
|
||||
await _updateArtists(localMusic['ARTISTS'] as List<ArtistModel>);
|
||||
await _updateAlbums(localMusic['ALBUMS'] as List<AlbumModel>);
|
||||
await _updateSongs(localMusic['SONGS'] as List<SongModel>);
|
||||
final artists = localMusic['ARTISTS'] as List<ArtistModel>;
|
||||
final albums = localMusic['ALBUMS'] as List<AlbumModel>;
|
||||
final songs = localMusic['SONGS'] as List<SongModel>;
|
||||
|
||||
_log.d('Artists found: ${artists.length}');
|
||||
_log.d('Albums found: ${albums.length}');
|
||||
_log.d('Songs found: ${songs.length}');
|
||||
|
||||
await _updateArtists(artists);
|
||||
await _updateAlbums(albums);
|
||||
await _updateSongs(songs);
|
||||
|
||||
_log.d('updateDatabase finished');
|
||||
}
|
||||
|
@ -219,11 +227,11 @@ class MusicDataRepositoryImpl implements MusicDataRepository {
|
|||
return songs
|
||||
..sort(
|
||||
(a, b) {
|
||||
final r = -a.likeCount.compareTo(b.likeCount);
|
||||
if (r != 0) {
|
||||
return r;
|
||||
}
|
||||
return -a.playCount.compareTo(b.playCount);
|
||||
int r = -a.likeCount.compareTo(b.likeCount);
|
||||
if (r == 0) r = -a.playCount.compareTo(b.playCount);
|
||||
if (r == 0) r = a.skipCount.compareTo(b.skipCount);
|
||||
if (r == 0) r = a.title.compareTo(b.title);
|
||||
return r;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -231,12 +239,9 @@ class MusicDataRepositoryImpl implements MusicDataRepository {
|
|||
List<Album> _sortArtistAlbums(List<Album> albums) {
|
||||
return albums
|
||||
..sort((a, b) {
|
||||
if (b.pubYear == null) {
|
||||
return -1;
|
||||
}
|
||||
if (a.pubYear == null) {
|
||||
return 1;
|
||||
}
|
||||
if (b.pubYear == null) return -1;
|
||||
if (a.pubYear == null) return 1;
|
||||
|
||||
return -a.pubYear!.compareTo(b.pubYear!);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -5,11 +5,11 @@ import '../datasources/settings_data_source.dart';
|
|||
|
||||
class SettingsRepositoryImpl implements SettingsRepository {
|
||||
SettingsRepositoryImpl(this._settingsDataSource) {
|
||||
_settingsDataSource.blockSkippedSongsThreshold.listen((event) {
|
||||
_settingsDataSource.blockSkippedSongsThreshold.listen((event) {
|
||||
_blockSkippedSongsThreshold.add(event);
|
||||
});
|
||||
|
||||
_settingsDataSource.isBlockSkippedSongsEnabled.listen((event) {
|
||||
_settingsDataSource.isBlockSkippedSongsEnabled.listen((event) {
|
||||
_isBlockSkippedSongsEnabled.add(event);
|
||||
});
|
||||
}
|
||||
|
@ -33,20 +33,4 @@ class SettingsRepositoryImpl implements SettingsRepository {
|
|||
if (path == null) return;
|
||||
await _settingsDataSource.removeLibraryFolder(path);
|
||||
}
|
||||
|
||||
@override
|
||||
ValueStream<int> get blockSkippedSongsThreshold => _blockSkippedSongsThreshold.stream;
|
||||
|
||||
@override
|
||||
ValueStream<bool> get isBlockSkippedSongsEnabled => _isBlockSkippedSongsEnabled.stream;
|
||||
|
||||
@override
|
||||
Future<void> setBlockSkippedSongsThreshold(int threshold) async {
|
||||
_settingsDataSource.setBlockSkippedSongsThreshold(threshold);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setBlockSkippedSongs(bool enabled) async {
|
||||
_settingsDataSource.setBlockSkippedSongs(enabled);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -73,6 +73,10 @@ flutter:
|
|||
# "family" key with the font family name, and a "fonts" key with a
|
||||
# list giving the asset and other descriptors for the font. For
|
||||
# example:
|
||||
fonts:
|
||||
- family: MuckeIcons
|
||||
fonts:
|
||||
- asset: fonts/MuckeIcons.ttf
|
||||
# fonts:
|
||||
# - family: Schyler
|
||||
# fonts:
|
||||
|
|