Compare commits
913 Commits
Author | SHA1 | Date |
---|---|---|
Anna Schumaker | 45d2422be3 | |
Anna Schumaker | 54138d8814 | |
Anna Schumaker | 6ab3cff28f | |
Anna Schumaker | 0972e027ed | |
Anna Schumaker | 77efa0c631 | |
Anna Schumaker | a9970c455f | |
Anna Schumaker | fc5e6eb043 | |
Anna Schumaker | a41652ab28 | |
Anna Schumaker | eca857cb3b | |
Anna Schumaker | d7fb67ed51 | |
Anna Schumaker | 7df129d533 | |
Anna Schumaker | a4cdac7f22 | |
Anna Schumaker | d5b0752497 | |
Anna Schumaker | 5fb46dc663 | |
Anna Schumaker | d149289e00 | |
Anna Schumaker | f167f968ba | |
Anna Schumaker | 94f3a7f387 | |
Anna Schumaker | 60e6e2a9eb | |
Anna Schumaker | 1836104f40 | |
Anna Schumaker | cd7364300e | |
Anna Schumaker | 994234caf2 | |
Anna Schumaker | af5bafb03e | |
Anna Schumaker | fd68cdf70a | |
Anna Schumaker | 8a2c631a9b | |
Anna Schumaker | e6ab06cf2b | |
Anna Schumaker | e6fb772cad | |
Anna Schumaker | cce8666140 | |
Anna Schumaker | edcba6a353 | |
Anna Schumaker | 59506d45e7 | |
Anna Schumaker | e1f13a7ef4 | |
Anna Schumaker | 111fcd4e72 | |
Anna Schumaker | c5494811f4 | |
Anna Schumaker | 07f832ad26 | |
Anna Schumaker | 35d53855f5 | |
Anna Schumaker | a848d5d03c | |
Anna Schumaker | 0aaafcb5f7 | |
Anna Schumaker | 8f13765b08 | |
Anna Schumaker | 84a1022bdf | |
Anna Schumaker | 659aaff6a1 | |
Anna Schumaker | 3736b6cf3b | |
Anna Schumaker | 3fdf89c75e | |
Anna Schumaker | 22854b2f25 | |
Anna Schumaker | b6d45e666e | |
Anna Schumaker | 842547d735 | |
Anna Schumaker | 198fbf7f9b | |
Anna Schumaker | 82280edfa2 | |
Anna Schumaker | 48f79bdb49 | |
Anna Schumaker | d7d553b80f | |
Anna Schumaker | a2854ef31e | |
Anna Schumaker | d96e8ca1ca | |
Anna Schumaker | 1940a31a77 | |
Anna Schumaker | 07196a7cc8 | |
Anna Schumaker | 64e27c1221 | |
Anna Schumaker | 31cda0eebd | |
Anna Schumaker | 9b9be4e322 | |
Anna Schumaker | 1374a025e1 | |
Anna Schumaker | 21e1796b14 | |
Anna Schumaker | b4347d5a34 | |
Anna Schumaker | 386514ac5c | |
Anna Schumaker | 7558a32940 | |
Anna Schumaker | a8f94e9443 | |
Anna Schumaker | cc464ed198 | |
Anna Schumaker | f9a573b6a3 | |
Anna Schumaker | e7ceed9b5d | |
Anna Schumaker | 497ed57057 | |
Anna Schumaker | e6f34d34f0 | |
Anna Schumaker | 4986bdad13 | |
Anna Schumaker | fc6e3ff464 | |
Anna Schumaker | c03530f318 | |
Anna Schumaker | 0f2e30589d | |
Anna Schumaker | 76e400e156 | |
Anna Schumaker | 9c6a9f7759 | |
Anna Schumaker | 1182e55df9 | |
Anna Schumaker | b558a9043c | |
Anna Schumaker | d95c693db2 | |
Anna Schumaker | 36349e9890 | |
Anna Schumaker | a7280356c8 | |
Anna Schumaker | 8b17962b4e | |
Anna Schumaker | 7e303fa2b1 | |
Anna Schumaker | 79accb5bb0 | |
Anna Schumaker | 8614aa37cd | |
Anna Schumaker | 4990d68711 | |
Anna Schumaker | fac383e9fc | |
Anna Schumaker | 466d9ce291 | |
Anna Schumaker | 3f9372051f | |
Anna Schumaker | 2cfccb8177 | |
Anna Schumaker | e773ae6f82 | |
Anna Schumaker | 042cddb65b | |
Anna Schumaker | 2a95031ee7 | |
Anna Schumaker | df21aa1299 | |
Anna Schumaker | 432d3e5d62 | |
Anna Schumaker | a15ad67029 | |
Anna Schumaker | 5247bf2de0 | |
Anna Schumaker | d3df9a69f2 | |
Anna Schumaker | f46ef37630 | |
Anna Schumaker | 76ebfaa6d4 | |
Anna Schumaker | cc5f65bf82 | |
Anna Schumaker | bb40ef479f | |
Anna Schumaker | d1c682501f | |
Anna Schumaker | 448b4a16f4 | |
Anna Schumaker | 3286b61dcf | |
Anna Schumaker | 7f54562b71 | |
Anna Schumaker | cdbf8b1736 | |
Anna Schumaker | 1dd0b7c2aa | |
Anna Schumaker | a808cac04c | |
Anna Schumaker | 1c6305e24e | |
Anna Schumaker | 8bf5aefd1a | |
Anna Schumaker | 03e7346900 | |
Anna Schumaker | 18f1bfe801 | |
Anna Schumaker | f25bdab367 | |
Anna Schumaker | 0c197c10f9 | |
Anna Schumaker | fda29aaf13 | |
Anna Schumaker | 2fb27178ee | |
Anna Schumaker | efbbc4ceff | |
Anna Schumaker | 92bb742f8f | |
Anna Schumaker | 6dbc574954 | |
Anna Schumaker | 859ff8656f | |
Anna Schumaker | a45c7d6889 | |
Anna Schumaker | ca6c5293c6 | |
Anna Schumaker | 287c6e0e9c | |
Anna Schumaker | 1d09e967d0 | |
Anna Schumaker | f670a3796b | |
Anna Schumaker | bd8df2a169 | |
Anna Schumaker | 07bf09c2ad | |
Anna Schumaker | e74c1c053c | |
Anna Schumaker | 793a8a5817 | |
Anna Schumaker | d2335f5c6e | |
Anna Schumaker | 37d95656e9 | |
Anna Schumaker | a87373f335 | |
Anna Schumaker | 93cb7145e6 | |
Anna Schumaker | ed095eb987 | |
Anna Schumaker | ee8825745b | |
Anna Schumaker | 4ad9b39398 | |
Anna Schumaker | ae604ab4a8 | |
Anna Schumaker | 2a8288e1b3 | |
Anna Schumaker | 8af9148606 | |
Anna Schumaker | 8a5163fb68 | |
Anna Schumaker | 8220dd9932 | |
Anna Schumaker | 0754f10883 | |
Anna Schumaker | a33decf549 | |
Anna Schumaker | 9fa5f0b0db | |
Anna Schumaker | b17585237a | |
Anna Schumaker | 7655b0cae8 | |
Anna Schumaker | 1fa31c51a6 | |
Anna Schumaker | 72d0f9a248 | |
Anna Schumaker | b5c1af263a | |
Anna Schumaker | 5c215df0bf | |
Anna Schumaker | 73825dd916 | |
Anna Schumaker | 09e358b96b | |
Anna Schumaker | bef0c70e5a | |
Anna Schumaker | 61e3137131 | |
Anna Schumaker | 0c1147513e | |
Anna Schumaker | 0e523ed279 | |
Anna Schumaker | 3364d4733e | |
Anna Schumaker | 1b9cdf5a5c | |
Anna Schumaker | 066027ecb6 | |
Anna Schumaker | ca5f0701e9 | |
Anna Schumaker | 846f7df9c1 | |
Anna Schumaker | 4d68ce8ce6 | |
Anna Schumaker | 249de0da21 | |
Anna Schumaker | 58cd43c330 | |
Anna Schumaker | 9b4a1785a1 | |
Anna Schumaker | c59b097638 | |
Anna Schumaker | 7e3e4194f3 | |
Anna Schumaker | 186367afe0 | |
Anna Schumaker | 62e494f2af | |
Anna Schumaker | 15ceda1194 | |
Anna Schumaker | 638caeaa91 | |
Anna Schumaker | a7565340d2 | |
Anna Schumaker | 0b231119d2 | |
Anna Schumaker | fc1e917aee | |
Anna Schumaker | bc1c462d36 | |
Anna Schumaker | 8fd4e4c637 | |
Anna Schumaker | 96e5749e7f | |
Anna Schumaker | 2a845feb38 | |
Anna Schumaker | f8c0668e5e | |
Anna Schumaker | 07d735eeee | |
Anna Schumaker | 943ab02aa5 | |
Anna Schumaker | 869e83b7bd | |
Anna Schumaker | 88bf71ac22 | |
Anna Schumaker | 3bf99b12eb | |
Anna Schumaker | e660e3f0b2 | |
Anna Schumaker | 3ae5e0f535 | |
Anna Schumaker | 39bc7492d4 | |
Anna Schumaker | e7d2fa5c4d | |
Anna Schumaker | 155b9c3ee6 | |
Anna Schumaker | 7fbef057bf | |
Anna Schumaker | d373c55320 | |
Anna Schumaker | f147c30c30 | |
Anna Schumaker | 1e6ab2e23c | |
Anna Schumaker | 83a21863b9 | |
Anna Schumaker | e876f8125f | |
Anna Schumaker | dc53ae271b | |
Anna Schumaker | c9e9e3a340 | |
Anna Schumaker | d818688bfd | |
Anna Schumaker | cebf2069cb | |
Anna Schumaker | 36f399ecb7 | |
Anna Schumaker | 83db8e4ae7 | |
Anna Schumaker | ad3e56250e | |
Anna Schumaker | bb673ddb62 | |
Anna Schumaker | 4fee5f85f0 | |
Anna Schumaker | ab47a7ac88 | |
Anna Schumaker | e550638823 | |
Anna Schumaker | 3e17b7bc1f | |
Anna Schumaker | b4e2770223 | |
Anna Schumaker | df2236db9f | |
Anna Schumaker | 1291a0d139 | |
Anna Schumaker | b3efd9d84d | |
Anna Schumaker | 828f861d9a | |
Anna Schumaker | 3562e164b0 | |
Anna Schumaker | a152ed689f | |
Anna Schumaker | a2113dc378 | |
Anna Schumaker | 9885c60bff | |
Anna Schumaker | 831a5379e5 | |
Anna Schumaker | f3f8ad91c6 | |
Anna Schumaker | 4fc8c72ea8 | |
Anna Schumaker | 18e76a7dca | |
Anna Schumaker | bbac5e23be | |
Anna Schumaker | c1c197acb5 | |
Anna Schumaker | 4c3405e874 | |
Anna Schumaker | b3074979f7 | |
Anna Schumaker | fd84222c2b | |
Anna Schumaker | 38cd2f761d | |
Anna Schumaker | a36fd137d5 | |
Anna Schumaker | a4049f8d01 | |
Anna Schumaker | 9b04ebcd71 | |
Anna Schumaker | d3f505465c | |
Anna Schumaker | 1d3438932e | |
Anna Schumaker | fa96def899 | |
Anna Schumaker | 2431ad104e | |
Anna Schumaker | 1c386809d0 | |
Anna Schumaker | dbc1df154a | |
Anna Schumaker | 730395aeff | |
Anna Schumaker | 9e37062920 | |
Anna Schumaker | df43010766 | |
Anna Schumaker | c49b77a24a | |
Anna Schumaker | 3fc19275f3 | |
Anna Schumaker | ac3c316d9a | |
Anna Schumaker | 32d712c213 | |
Anna Schumaker | 39d87f98f8 | |
Anna Schumaker | b9916706f1 | |
Anna Schumaker | ad29c520d4 | |
Anna Schumaker | f550d45811 | |
Anna Schumaker | 6325779062 | |
Anna Schumaker | 27a1e2e12a | |
Anna Schumaker | 619ea0b559 | |
Anna Schumaker | 010969c7b3 | |
Anna Schumaker | 82da46365f | |
Anna Schumaker | e522475d38 | |
Anna Schumaker | cdbe96c575 | |
Anna Schumaker | 453f176d63 | |
Anna Schumaker | 3383f9e32a | |
Anna Schumaker | f9238c34e4 | |
Anna Schumaker | 90b80fc8a7 | |
Anna Schumaker | 133efc0515 | |
Anna Schumaker | 85bb67feed | |
Anna Schumaker | e7b4973a50 | |
Anna Schumaker | 7d0dbcbdc7 | |
Anna Schumaker | a89dc49609 | |
Anna Schumaker | 34f21bf247 | |
Anna Schumaker | 63951568af | |
Anna Schumaker | be3b788380 | |
Anna Schumaker | 9df093ec85 | |
Anna Schumaker | 50207c5b79 | |
Anna Schumaker | 01927cf806 | |
Anna Schumaker | 59b2c854e7 | |
Anna Schumaker | e4e90f165e | |
Anna Schumaker | 4250757b83 | |
Anna Schumaker | cb2af114c5 | |
Anna Schumaker | b4d78b0d3d | |
Anna Schumaker | d5de8e5dbf | |
Anna Schumaker | 6120366da3 | |
Anna Schumaker | eb16d9c6fb | |
Anna Schumaker | 1d7cfa0e6d | |
Anna Schumaker | 99faa0cf39 | |
Anna Schumaker | 0407486316 | |
Anna Schumaker | 9a216fee1e | |
Anna Schumaker | ae916eaf40 | |
Anna Schumaker | 9d279fe21e | |
Anna Schumaker | e25b8407b0 | |
Anna Schumaker | d7709fd5fb | |
Anna Schumaker | eb8a23f2ff | |
Anna Schumaker | 811509ff80 | |
Anna Schumaker | cf4eedb592 | |
Anna Schumaker | 93d1550763 | |
Anna Schumaker | 2e38b3551e | |
Anna Schumaker | ec3b9f7c8e | |
Anna Schumaker | 142af976b3 | |
Anna Schumaker | 5a41eef8a2 | |
Anna Schumaker | 419d31d4c3 | |
Anna Schumaker | afbf9e0b1a | |
Anna Schumaker | ef8a764780 | |
Anna Schumaker | 27436115c7 | |
Anna Schumaker | 1b18177d0a | |
Anna Schumaker | 21a47ec3b8 | |
Anna Schumaker | b39c4c9ba4 | |
Anna Schumaker | 43a0ffd54c | |
Anna Schumaker | 0cb44aaf3a | |
Anna Schumaker | 4a4ec3fa36 | |
Anna Schumaker | d8c3fb9ace | |
Anna Schumaker | f02dff4177 | |
Anna Schumaker | ddb564ed82 | |
Anna Schumaker | ecebd20a0b | |
Anna Schumaker | f0e9e1f1e4 | |
Anna Schumaker | e2d4dd61fb | |
Anna Schumaker | 1b9101cf26 | |
Anna Schumaker | dcbf2dff72 | |
Anna Schumaker | d6e5e6c773 | |
Anna Schumaker | 85bfe717dd | |
Anna Schumaker | 30cebb4a45 | |
Anna Schumaker | 784cd3eb91 | |
Anna Schumaker | 71ee59ae22 | |
Anna Schumaker | 0cefd158d9 | |
Anna Schumaker | 0ea75ccb29 | |
Anna Schumaker | ffd09d410c | |
Anna Schumaker | a85a1df7de | |
Anna Schumaker | 50db0db06a | |
Anna Schumaker | 86d7fe43ed | |
Anna Schumaker | 8d9139aea5 | |
Anna Schumaker | 2f098a2af6 | |
Anna Schumaker | 939ebeac13 | |
Anna Schumaker | f726b6994c | |
Anna Schumaker | 54bb0ffe2b | |
Anna Schumaker | a4692c0fe0 | |
Anna Schumaker | d33245f604 | |
Anna Schumaker | 6fc965e80d | |
Anna Schumaker | 33894d7068 | |
Anna Schumaker | 87f0847f91 | |
Anna Schumaker | 28e5045975 | |
Anna Schumaker | 84661c4797 | |
Anna Schumaker | 278a54f7ec | |
Anna Schumaker | c6e9c176d8 | |
Anna Schumaker | 7a79ac26b0 | |
Anna Schumaker | 64ceca84c5 | |
Anna Schumaker | 8985e70439 | |
Anna Schumaker | bfcfaae977 | |
Anna Schumaker | da19ddd388 | |
Anna Schumaker | 5232217eeb | |
Anna Schumaker | e4cd59b895 | |
Anna Schumaker | 69e628f505 | |
Anna Schumaker | 69b39ea717 | |
Anna Schumaker | 017da02827 | |
Anna Schumaker | eb1361248d | |
Anna Schumaker | a775eeb761 | |
Anna Schumaker | 903357395b | |
Anna Schumaker | 7e00c8ed10 | |
Anna Schumaker | 524d1886f9 | |
Anna Schumaker | a8abbdfdcc | |
Anna Schumaker | 19ca9d932b | |
Anna Schumaker | 59839547e2 | |
Anna Schumaker | dd268a24fe | |
Anna Schumaker | 0470c6efa6 | |
Anna Schumaker | 4e9c5e0a00 | |
Anna Schumaker | 6a1c27e9a4 | |
Anna Schumaker | b2dbbbaae6 | |
Anna Schumaker | c671c7aefc | |
Anna Schumaker | 3e2ed7761c | |
Anna Schumaker | 1710c8076a | |
Anna Schumaker | 8f8e4a4459 | |
Anna Schumaker | 926ca09275 | |
Anna Schumaker | 7e09375325 | |
Anna Schumaker | 9b7c45634c | |
Anna Schumaker | 487274ff00 | |
Anna Schumaker | c67d09740c | |
Anna Schumaker | f9dd51170d | |
Anna Schumaker | 8327d77ddd | |
Anna Schumaker | 5e65414263 | |
Anna Schumaker | 921a04e28e | |
Anna Schumaker | ca2f35a848 | |
Anna Schumaker | 3b03301e61 | |
Anna Schumaker | 65bbd21669 | |
Anna Schumaker | 8c875acec9 | |
Anna Schumaker | 1b83978b09 | |
Anna Schumaker | 10f81461d9 | |
Anna Schumaker | 4fb854d0b1 | |
Anna Schumaker | fc88f68d6b | |
Anna Schumaker | 9f2811a343 | |
Anna Schumaker | 2f559b2fb3 | |
Anna Schumaker | 0e171012ce | |
Anna Schumaker | 17150b1791 | |
Anna Schumaker | 257ef3612f | |
Anna Schumaker | b35b1eb132 | |
Anna Schumaker | 1bc43a9e2c | |
Anna Schumaker | 336c0a01af | |
Anna Schumaker | 65b43c7ae5 | |
Anna Schumaker | 40bce8cc59 | |
Anna Schumaker | 5c426fc8d7 | |
Anna Schumaker | f58cc8da46 | |
Anna Schumaker | a28b5c4ec5 | |
Anna Schumaker | a9aa3c297d | |
Anna Schumaker | 53285534b9 | |
Anna Schumaker | c2178bc265 | |
Anna Schumaker | e82beb719f | |
Anna Schumaker | d222c306e0 | |
Anna Schumaker | 65b547f60b | |
Anna Schumaker | 629f81da17 | |
Anna Schumaker | 76d8b00ecc | |
Anna Schumaker | 00c4c8a418 | |
Anna Schumaker | ae851ab4ea | |
Anna Schumaker | 21718c4179 | |
Anna Schumaker | 15807434a4 | |
Anna Schumaker | 1d02024505 | |
Anna Schumaker | 20f10c163d | |
Anna Schumaker | 4e5ae5e57c | |
Anna Schumaker | d7322c1f07 | |
Anna Schumaker | 11ef52b5de | |
Anna Schumaker | aead4939c3 | |
Anna Schumaker | 2e753b6f52 | |
Anna Schumaker | d16e06111d | |
Anna Schumaker | 6ad4325f22 | |
Anna Schumaker | 2e1c27294b | |
Anna Schumaker | 1ca9bb36c1 | |
Anna Schumaker | a70b27779f | |
Anna Schumaker | b643b532d7 | |
Anna Schumaker | be29f53eaa | |
Anna Schumaker | c3bc8e9c02 | |
Anna Schumaker | f70920015c | |
Anna Schumaker | de0446120e | |
Anna Schumaker | b3750aa31c | |
Anna Schumaker | d3e2f069fd | |
Anna Schumaker | 81aea3017e | |
Anna Schumaker | f4b12c5a83 | |
Anna Schumaker | 532d55ba4f | |
Anna Schumaker | 019137e4ed | |
Anna Schumaker | dddb098354 | |
Anna Schumaker | 1a29458d7c | |
Anna Schumaker | c2a7d0289f | |
Anna Schumaker | 8f7e8be39d | |
Anna Schumaker | a80a84a955 | |
Anna Schumaker | d460bcaee8 | |
Anna Schumaker | 3d85f0fdc5 | |
Anna Schumaker | 1633946981 | |
Anna Schumaker | 3806577154 | |
Anna Schumaker | a430c5b117 | |
Anna Schumaker | 3d31349cca | |
Anna Schumaker | f855eaea58 | |
Anna Schumaker | 5964c508ce | |
Anna Schumaker | 73b33e9718 | |
Anna Schumaker | f3360f6da5 | |
Anna Schumaker | 9733b82ae8 | |
Anna Schumaker | bddbd04ef5 | |
Anna Schumaker | b3476e15e9 | |
Anna Schumaker | 73c70678bb | |
Anna Schumaker | 62e1b27b6c | |
Anna Schumaker | 382ee79c2e | |
Anna Schumaker | f2597a8e6c | |
Anna Schumaker | 8abc45b1ae | |
Anna Schumaker | 871bf88b94 | |
Anna Schumaker | f70b2b940f | |
Anna Schumaker | 24448aefec | |
Anna Schumaker | 12ae7cfee6 | |
Anna Schumaker | 11430f89cf | |
Anna Schumaker | e1a722d04b | |
Anna Schumaker | c448db2665 | |
Anna Schumaker | 78aa0f9ff6 | |
Anna Schumaker | 7f867199cb | |
Anna Schumaker | 1599897fbf | |
Anna Schumaker | c5a0a3470e | |
Anna Schumaker | 4d5569ef7a | |
Anna Schumaker | 261f6d91d7 | |
Anna Schumaker | e4930704a2 | |
Anna Schumaker | 03ed7c4b84 | |
Anna Schumaker | 20e0a85a5d | |
Anna Schumaker | b3922cc731 | |
Anna Schumaker | e41f554b2e | |
Anna Schumaker | 938bbc92f2 | |
Anna Schumaker | 6e02f75262 | |
Anna Schumaker | cfeca9ae4b | |
Anna Schumaker | 6019f07bd3 | |
Anna Schumaker | 004d4578d4 | |
Anna Schumaker | 085701ad9d | |
Anna Schumaker | f7e9e8b321 | |
Anna Schumaker | 44a6ad0d78 | |
Anna Schumaker | e578e0e6dd | |
Anna Schumaker | 34447007f9 | |
Anna Schumaker | 5a54fb69b6 | |
Anna Schumaker | dc07d637f6 | |
Anna Schumaker | 64fc5a9a0d | |
Anna Schumaker | 15ed068e13 | |
Anna Schumaker | 696173b03e | |
Anna Schumaker | dfae74dd50 | |
Anna Schumaker | 8eaf6506e8 | |
Anna Schumaker | efae58c356 | |
Anna Schumaker | 6265b937c7 | |
Anna Schumaker | 5d4de9c5b0 | |
Anna Schumaker | c7bc2062d2 | |
Anna Schumaker | 62123eb025 | |
Anna Schumaker | 44a57ed863 | |
Anna Schumaker | 7f0e1ecc99 | |
Anna Schumaker | 92a53d90b4 | |
Anna Schumaker | fa0dee5921 | |
Anna Schumaker | 98fe6d3bbb | |
Anna Schumaker | 93fb40360d | |
Anna Schumaker | fda3f761a0 | |
Anna Schumaker | 63b46b6161 | |
Anna Schumaker | 29b2b6f6f4 | |
Anna Schumaker | 415bd9731a | |
Anna Schumaker | 04a175d3e3 | |
Anna Schumaker | ee4bbacf81 | |
Anna Schumaker | da3dd60ef3 | |
Anna Schumaker | 951624c82f | |
Anna Schumaker | 17910c72b2 | |
Anna Schumaker | 779969f28b | |
Anna Schumaker | bd8f3754e7 | |
Anna Schumaker | 1d1e41916a | |
Anna Schumaker | 3098282382 | |
Anna Schumaker | 3b62125135 | |
Anna Schumaker | 4de869d691 | |
Anna Schumaker | 160295a6cc | |
Anna Schumaker | 343dec5d8d | |
Anna Schumaker | 7dff5412bc | |
Anna Schumaker | 83724c5f6f | |
Anna Schumaker | 8893cdb58b | |
Anna Schumaker | c5a563c112 | |
Anna Schumaker | 6ab5d99d71 | |
Anna Schumaker | e24ab97545 | |
Anna Schumaker | 2c18f9e715 | |
Anna Schumaker | 00dd72d46e | |
Anna Schumaker | 5e9b6bc975 | |
Anna Schumaker | 90e0b3ed78 | |
Anna Schumaker | 2ff7113668 | |
Anna Schumaker | 9d3cc2e5ab | |
Anna Schumaker | 5ab2e63734 | |
Anna Schumaker | 1527ee0e6d | |
Anna Schumaker | 5a0fc355e4 | |
Anna Schumaker | a465577c86 | |
Anna Schumaker | e3d4143565 | |
Anna Schumaker | 140abe79bc | |
Anna Schumaker | 8026207037 | |
Anna Schumaker | 0f5d4e6a34 | |
Anna Schumaker | 151c446635 | |
Anna Schumaker | e5dc36ca6e | |
Anna Schumaker | 90fc9bff0a | |
Anna Schumaker | c845530812 | |
Anna Schumaker | 9a98a009e9 | |
Anna Schumaker | 9ec2497ac5 | |
Anna Schumaker | afb2653f65 | |
Anna Schumaker | 378ff72307 | |
Anna Schumaker | db01ed3208 | |
Anna Schumaker | 2b426cff48 | |
Anna Schumaker | 9835235acd | |
Anna Schumaker | 6bb08ddbaa | |
Anna Schumaker | e8d7576704 | |
Anna Schumaker | 5a30a63904 | |
Anna Schumaker | b7cf2509f7 | |
Anna Schumaker | 9928c3239f | |
Anna Schumaker | 4f1f76b764 | |
Anna Schumaker | eb5c0185e7 | |
Anna Schumaker | 617088c89b | |
Anna Schumaker | c62a88ce09 | |
Anna Schumaker | e30f7f8115 | |
Anna Schumaker | 80d921343d | |
Anna Schumaker | 99b51f5257 | |
Anna Schumaker | df3436f68a | |
Anna Schumaker | c8ccf7b844 | |
Anna Schumaker | b4218833c5 | |
Anna Schumaker | 2ea5104d42 | |
Anna Schumaker | 86b63298a5 | |
Anna Schumaker | 3c9798a6b7 | |
Anna Schumaker | ecc7843ea5 | |
Anna Schumaker | dd66b10dfa | |
Anna Schumaker | 9ef9b17b93 | |
Anna Schumaker | 1ac9e69128 | |
Anna Schumaker | cbb9631877 | |
Anna Schumaker | 1e59968154 | |
Anna Schumaker | 5b7a985dc6 | |
Anna Schumaker | 54af68a57f | |
Anna Schumaker | d0dddeacdb | |
Anna Schumaker | 940d2d1dc2 | |
Anna Schumaker | 570efda299 | |
Anna Schumaker | d79eb2c9b8 | |
Anna Schumaker | 0d9b7baff6 | |
Anna Schumaker | 97c8f80393 | |
Anna Schumaker | 6b52775e58 | |
Anna Schumaker | 6a44f9e1a1 | |
Anna Schumaker | a6bfbbb1c6 | |
Anna Schumaker | 0344a2088d | |
Anna Schumaker | 9db9df619f | |
Anna Schumaker | e137eb0108 | |
Anna Schumaker | 3723dff87d | |
Anna Schumaker | 79ecaa11fb | |
Anna Schumaker | 5dcf681e48 | |
Anna Schumaker | b5e13af30c | |
Anna Schumaker | 12949651f7 | |
Anna Schumaker | 9e0f017e61 | |
Anna Schumaker | fe6f31b1e5 | |
Anna Schumaker | ee4f0d4c89 | |
Anna Schumaker | c308ba7f8e | |
Anna Schumaker | 224290d4eb | |
Anna Schumaker | 427319b72e | |
Anna Schumaker | 98faba93d1 | |
Anna Schumaker | 096c0e8eaa | |
Anna Schumaker | c4e0efbe72 | |
Anna Schumaker | 2f3bdf353d | |
Anna Schumaker | ea10b7e630 | |
Anna Schumaker | 4998b0867b | |
Anna Schumaker | 1973d15605 | |
Anna Schumaker | a97ec66e7f | |
Anna Schumaker | d825e2d481 | |
Anna Schumaker | f1bcc7746e | |
Anna Schumaker | bc513532c1 | |
Anna Schumaker | fff2da5439 | |
Anna Schumaker | d0179a9bfa | |
Anna Schumaker | 9a209d098b | |
Anna Schumaker | 223a1e47c1 | |
Anna Schumaker | 0bcfa06bfb | |
Anna Schumaker | 2ab0c6957b | |
Anna Schumaker | f706ccb77c | |
Anna Schumaker | bd1e20bc56 | |
Anna Schumaker | 279d6e0228 | |
Anna Schumaker | cb6d451fb1 | |
Anna Schumaker | f01cce3b47 | |
Anna Schumaker | fc03b6cb95 | |
Anna Schumaker | f50c1541a1 | |
Anna Schumaker | dfb023a0fc | |
Anna Schumaker | a89aa5a201 | |
Anna Schumaker | b2a61a60f3 | |
Anna Schumaker | 1ad112e217 | |
Anna Schumaker | df93ad06df | |
Anna Schumaker | 3a08828dda | |
Anna Schumaker | 6c8733bbbb | |
Anna Schumaker | 490370b759 | |
Anna Schumaker | 061dbc4575 | |
Anna Schumaker | 4dbaf12701 | |
Anna Schumaker | 108dbacf41 | |
Anna Schumaker | 2c65584e2d | |
Anna Schumaker | e97939a9cd | |
Anna Schumaker | c0a6c663d1 | |
Anna Schumaker | bd22c8da6d | |
Anna Schumaker | 27a4cc23ed | |
Anna Schumaker | e290552812 | |
Anna Schumaker | 5a9c6a1dd1 | |
Anna Schumaker | d769140e35 | |
Anna Schumaker | 7995e381c0 | |
Anna Schumaker | a1c6502226 | |
Anna Schumaker | f328b1686d | |
Anna Schumaker | d5c365ddf0 | |
Anna Schumaker | cacd344741 | |
Anna Schumaker | 8e6b963eb8 | |
Anna Schumaker | c9eb9d724b | |
Anna Schumaker | aee4764a2c | |
Anna Schumaker | 8d2b0caf6c | |
Anna Schumaker | 87c59ce888 | |
Anna Schumaker | 2d1ddd5a35 | |
Anna Schumaker | bbf111ecb0 | |
Anna Schumaker | 76a6eba537 | |
Anna Schumaker | 4c784366d7 | |
Anna Schumaker | 13525eda22 | |
Anna Schumaker | c5e03d98cf | |
Anna Schumaker | f42c3d41d1 | |
Anna Schumaker | d07b7e68ac | |
Anna Schumaker | 4fd4f75f0e | |
Anna Schumaker | b01f61f8e4 | |
Anna Schumaker | d874c0e408 | |
Anna Schumaker | b59b2a2daf | |
Anna Schumaker | bf73685098 | |
Anna Schumaker | ac468ec3de | |
Anna Schumaker | cdf91cc59b | |
Anna Schumaker | 48b59bd6c4 | |
Anna Schumaker | e5711c26f0 | |
Anna Schumaker | b6488b6c39 | |
Anna Schumaker | ada392550a | |
Anna Schumaker | ea149d57df | |
Anna Schumaker | 87e547d85d | |
Anna Schumaker | e8681e4c92 | |
Anna Schumaker | 70803cc199 | |
Anna Schumaker | 4fd370ceb6 | |
Anna Schumaker | dd2adc6307 | |
Anna Schumaker | ccf6e4c61f | |
Anna Schumaker | ba59d6bd29 | |
Anna Schumaker | 13223427c1 | |
Anna Schumaker | b34fd6d522 | |
Anna Schumaker | ed69e3d2b8 | |
Anna Schumaker | e59617ffa7 | |
Anna Schumaker | 9c3adf2b09 | |
Anna Schumaker | eb481b8d09 | |
Anna Schumaker | ff81af24fc | |
Anna Schumaker | 6acee801ab | |
Anna Schumaker | f5fba1680a | |
Anna Schumaker | ddcb973d8e | |
Anna Schumaker | a313160f94 | |
Anna Schumaker | 21eb09ced3 | |
Anna Schumaker | bc9efade75 | |
Anna Schumaker | d19e0b2cc0 | |
Anna Schumaker | 24f53d88b6 | |
Anna Schumaker | ddbc040a3b | |
Anna Schumaker | 2c676631a4 | |
Anna Schumaker | 410a784ede | |
Anna Schumaker | ce8ec63886 | |
Anna Schumaker | f098811a28 | |
Anna Schumaker | 2d6e42832b | |
Anna Schumaker | f85ad3a9c3 | |
Anna Schumaker | 5bc9f65cd9 | |
Anna Schumaker | 2770403c61 | |
Anna Schumaker | 5419711409 | |
Anna Schumaker | 8bb7a20ff4 | |
Anna Schumaker | 6e3027d087 | |
Anna Schumaker | f2222c1063 | |
Anna Schumaker | 13dadd25ef | |
Anna Schumaker | 548852dc24 | |
Anna Schumaker | 909b50168f | |
Anna Schumaker | 25211e93ac | |
Anna Schumaker | d2c4a36945 | |
Anna Schumaker | b91b898913 | |
Anna Schumaker | 02f5b13650 | |
Anna Schumaker | 5c34576e70 | |
Anna Schumaker | 79f79405a8 | |
Anna Schumaker | b4c81a4441 | |
Anna Schumaker | 9f3b8ae239 | |
Anna Schumaker | 088c2ec37f | |
Anna Schumaker | bb9f220785 | |
Anna Schumaker | 8a72637fe9 | |
Anna Schumaker | 16b055f06b | |
Anna Schumaker | 839f4c4aa1 | |
Anna Schumaker | c663f287d4 | |
Anna Schumaker | eb137c4339 | |
Anna Schumaker | 6c653c6a79 | |
Anna Schumaker | 84ff59cf81 | |
Anna Schumaker | 9ee00f9997 | |
Anna Schumaker | 7439a2a73d | |
Anna Schumaker | fde59a1868 | |
Anna Schumaker | be6895630d | |
Anna Schumaker | ca88bca999 | |
Anna Schumaker | 3c3f11b958 | |
Anna Schumaker | 8807f06fd6 | |
Anna Schumaker | 2cae2cd525 | |
Anna Schumaker | 0afe46b5df | |
Anna Schumaker | 8dece02eed | |
Anna Schumaker | 2f9ee44000 | |
Anna Schumaker | 08910bef7c | |
Anna Schumaker | f34e2cb57b | |
Anna Schumaker | eb81fa2e20 | |
Anna Schumaker | 480090f78d | |
Anna Schumaker | 58d61f52e2 | |
Anna Schumaker | 870a2b769b | |
Anna Schumaker | 0c214dd1a9 | |
Anna Schumaker | 29985d2a69 | |
Anna Schumaker | 684d52c522 | |
Anna Schumaker | 0c8bcfa552 | |
Anna Schumaker | 066d27a240 | |
Anna Schumaker | 70dca79b41 | |
Anna Schumaker | 44dbbff04b | |
Anna Schumaker | acd9dccb56 | |
Anna Schumaker | c01561831c | |
Anna Schumaker | e73ae0612d | |
Anna Schumaker | 539b2b5ac3 | |
Anna Schumaker | f5af640202 | |
Anna Schumaker | cb26d08315 | |
Anna Schumaker | 6aa64f0bc1 | |
Anna Schumaker | 19afc4aeef | |
Anna Schumaker | 2097f63f3d | |
Anna Schumaker | 5a4d5bcc44 | |
Anna Schumaker | 811270a832 | |
Anna Schumaker | abf9cfe90e | |
Anna Schumaker | fa748a2eb1 | |
Anna Schumaker | 7f0326be29 | |
Anna Schumaker | 4bbf9472a5 | |
Anna Schumaker | 627619c391 | |
Anna Schumaker | 5e96f40cf8 | |
Anna Schumaker | fd28d6830c | |
Anna Schumaker | d8754e9a45 | |
Anna Schumaker | f95a6d02ed | |
Anna Schumaker | 286085d248 | |
Anna Schumaker | 2d45e9e5c4 | |
Anna Schumaker | 36b8b29b0b | |
Anna Schumaker | e5e2c87a86 | |
Anna Schumaker | 50a7c83d61 | |
Anna Schumaker | c48e128d0a | |
Anna Schumaker | 5368208a37 | |
Anna Schumaker | dee8b948b0 | |
Anna Schumaker | bf39724a40 | |
Anna Schumaker | 71c4e6a1aa | |
Anna Schumaker | 15da6b4249 | |
Anna Schumaker | aacc1f876a | |
Anna Schumaker | 6723ef88c7 | |
Anna Schumaker | 8ae31ed483 | |
Anna Schumaker | 00899873bd | |
Anna Schumaker | 6680cdbd0e | |
Anna Schumaker | f31a67f644 | |
Anna Schumaker | 44d7f9a853 | |
Anna Schumaker | 61436d5ccd | |
Anna Schumaker | 0eb8521094 | |
Anna Schumaker | d4b27316c0 | |
Anna Schumaker | 961659bc66 | |
Anna Schumaker | 3bc5b1db9e | |
Anna Schumaker | 27e09f47a6 | |
Anna Schumaker | c479042a86 | |
Anna Schumaker | d292879837 | |
Anna Schumaker | a5f0da7a60 | |
Anna Schumaker | 92c1b64b1a | |
Anna Schumaker | f23fb9e0f5 | |
Anna Schumaker | 727752df08 | |
Anna Schumaker | 09e900eadf | |
Anna Schumaker | 8034a64f17 | |
Anna Schumaker | 711eb67f44 | |
Anna Schumaker | cf861473df | |
Anna Schumaker | fc3f2d8491 | |
Anna Schumaker | aac32681e5 | |
Anna Schumaker | df2d365b80 | |
Anna Schumaker | eba243a22a | |
Anna Schumaker | d81ef77fb3 | |
Anna Schumaker | ece432bb22 | |
Anna Schumaker | 0cf5187867 | |
Anna Schumaker | ed5f035855 | |
Anna Schumaker | 2586348921 | |
Anna Schumaker | a59a71eb38 | |
Anna Schumaker | 04291f4296 | |
Anna Schumaker | 03196c3efa | |
Anna Schumaker | 1d76990fd1 | |
Anna Schumaker | a5556ac10a | |
Anna Schumaker | 276c56406a | |
Anna Schumaker | d18d3dd214 | |
Anna Schumaker | 3f25369444 | |
Anna Schumaker | 7765c2bf57 | |
Anna Schumaker | 2511db3a08 | |
Anna Schumaker | 0a1a9f1394 | |
Anna Schumaker | d1d722a163 | |
Anna Schumaker | e1abe94b28 | |
Anna Schumaker | 20bb01ef4e | |
Anna Schumaker | 566ef3bb50 | |
Anna Schumaker | 9097bd0ffc | |
Anna Schumaker | 16c4c23a42 | |
Anna Schumaker | e5c3d8f02e | |
Anna Schumaker | e0c89556ed | |
Anna Schumaker | d9540b29d6 | |
Anna Schumaker | b7bd7622b3 | |
Anna Schumaker | 4323461bc6 | |
Anna Schumaker | 6358124ce8 | |
Anna Schumaker | aad90782ba | |
Anna Schumaker | 08bf842767 | |
Anna Schumaker | 76985c8842 | |
Anna Schumaker | 5979d1dcaf | |
Anna Schumaker | 2dd4f93bd0 | |
Anna Schumaker | fea5da5c10 | |
Anna Schumaker | 7ff3dc92a0 | |
Anna Schumaker | a248f8ebf7 | |
Anna Schumaker | 06ea2d8c0e | |
Anna Schumaker | 882ee6e1e7 | |
Anna Schumaker | 187c56f328 | |
Anna Schumaker | 5c92d0771f | |
Anna Schumaker | 15f5b8fc87 | |
Anna Schumaker | 17981f4393 | |
Anna Schumaker | 27eab88b9c | |
Anna Schumaker | 9633806c2d | |
Anna Schumaker | 2102b1bfc9 | |
Anna Schumaker | b919843717 | |
Anna Schumaker | b3d95a06d4 | |
Anna Schumaker | ecda136015 | |
Anna Schumaker | c22b93b2cf | |
Anna Schumaker | 6bdc481cb4 | |
Anna Schumaker | 06d62240bb | |
Anna Schumaker | bce2ba33f7 | |
Anna Schumaker | 8c7550d5c3 | |
Anna Schumaker | 51379c7e8c | |
Anna Schumaker | c125b3893e | |
Anna Schumaker | 75c039f22d | |
Anna Schumaker | 77d4815eb6 | |
Anna Schumaker | 460aeded4e | |
Anna Schumaker | 665e54a3bc | |
Anna Schumaker | b191a7f22c | |
Anna Schumaker | 8c8ab2a9eb | |
Anna Schumaker | b3592e40c0 | |
Anna Schumaker | 7cdc5678c6 | |
Anna Schumaker | 6e838ffba3 | |
Anna Schumaker | 126ed4c15c | |
Anna Schumaker | 743a464f3f | |
Anna Schumaker | 5e74385fbb | |
Anna Schumaker | 8ad45ebef3 | |
Anna Schumaker | 850af16c87 | |
Anna Schumaker | 5b921e7277 | |
Anna Schumaker | 8a59485cbc | |
Anna Schumaker | 5d251365fd | |
Anna Schumaker | bc0437b3fe | |
Anna Schumaker | fef3ef3353 | |
Anna Schumaker | beda9bfa3f | |
Anna Schumaker | 3b5bbf14af | |
Anna Schumaker | 20825613e9 | |
Anna Schumaker | 02c82c182e | |
Anna Schumaker | 5844402ee3 | |
Anna Schumaker | b7a4bb3872 | |
Anna Schumaker | 88f6fc526c | |
Anna Schumaker | 135e0d95d6 | |
Anna Schumaker | 7a32fdb982 | |
Anna Schumaker | 73992ed912 | |
Anna Schumaker | 4e6028cf6e | |
Anna Schumaker | 0e3d6251d3 | |
Anna Schumaker | 82ed2f2dc0 | |
Anna Schumaker | 6f0b95608a | |
Anna Schumaker | 1d3a762936 | |
Anna Schumaker | 1f5ce9e0d1 | |
Anna Schumaker | b1c5d42387 | |
Anna Schumaker | ce618e35a1 | |
Anna Schumaker | 65a81e5bb2 | |
Anna Schumaker | 7fb584581b | |
Anna Schumaker | 219f8fa119 | |
Anna Schumaker | e22533e7ec | |
Anna Schumaker | 3ecb06bcb6 | |
Anna Schumaker | 94f22c9fc5 | |
Anna Schumaker | 696933c6b4 | |
Anna Schumaker | 7065b6312e | |
Anna Schumaker | 49457751c9 | |
Anna Schumaker | 8e5cc543c3 | |
Anna Schumaker | ffa12a2c38 | |
Anna Schumaker | 79af4513b0 | |
Anna Schumaker | f413d14720 | |
Anna Schumaker | 295a4424ed | |
Anna Schumaker | 89ce6d725c | |
Anna Schumaker | 62f0606cd6 | |
Anna Schumaker | 8a1f5403d0 | |
Anna Schumaker | 0cb16f5c64 | |
Anna Schumaker | 6e39e17060 | |
Anna Schumaker | 3204fc0e44 | |
Anna Schumaker | d97ae38cc2 |
|
@ -13,3 +13,10 @@ bin/
|
|||
*.gcno
|
||||
core.*
|
||||
.debug
|
||||
CMakeCache.txt
|
||||
CMakeFiles
|
||||
Makefile
|
||||
cmake_install.cmake
|
||||
install_manifest.txt
|
||||
CTestTestfile.cmake
|
||||
Testing/
|
||||
|
|
|
@ -1,5 +1,14 @@
|
|||
job1:
|
||||
script: "scons"
|
||||
|
||||
job2:
|
||||
scrpt: "scons tests/core"
|
||||
stages:
|
||||
- build
|
||||
- test
|
||||
|
||||
build:
|
||||
stage: build
|
||||
script:
|
||||
- cmake -DCONFIG_DEBUG=ON && make
|
||||
|
||||
test:
|
||||
stage: test
|
||||
script:
|
||||
- cmake -DCONFIG_TESTING_VERBOSE=ON -DCONFIG_TESTING_GUI=OFF && make tests
|
||||
|
|
|
@ -0,0 +1,148 @@
|
|||
6.5.0:
|
||||
- Add a switch to disable GUI tests
|
||||
- Don't save and restore the "Last Played" tracks column width
|
||||
- Save databases if they have been upgraded
|
||||
- Don't duplicate tracks in the library after a defrag
|
||||
- Remove "Add New ..." menubutton and replace with Add Library Path button
|
||||
- Various UI tweaks
|
||||
- Fix showing the first playlist in the list
|
||||
- Fix memory leaks in core/
|
||||
- Fix theme parsing errors
|
||||
- Fix crash while reading 6.4.x playlists
|
||||
|
||||
6.5.0-rc:
|
||||
- Convert to CMake/CTest
|
||||
- Save current track in settings database
|
||||
- Add "Queued Tracks" playlist
|
||||
- Add support for changing current playlist
|
||||
- Hide empty playlists
|
||||
- Save current position in playlists
|
||||
- Set file version from Ocarina minor version
|
||||
- Add support for defragmenting tag databases on startup
|
||||
- Add user created playlists
|
||||
- Add support for changing volume
|
||||
- Remove Repeat button
|
||||
- Various UI improvements
|
||||
- Add command line options
|
||||
|
||||
6.4.20:
|
||||
- Enable idle processing when tracks are played to update dynamic playlists
|
||||
|
||||
6.4.20-rc:
|
||||
- Use a single GtkTreeModelFilter instance for displaying tracks
|
||||
- Use a single GuiQueueModel instance for displaying tracks
|
||||
- Reduce use of database entry index accesses
|
||||
- Reduce the size of the track tag
|
||||
|
||||
6.4.19:
|
||||
- Fix memory corruption when adding new library playlists
|
||||
- Use Enter key to accept new library paths
|
||||
- Remove deleted tracks from artist playlists
|
||||
- File chooser selects user's music directory
|
||||
- Remove deleted tracks from artist playlists
|
||||
|
||||
6.4.19-rc:
|
||||
- Allow filtering specific fields
|
||||
- Replace filtering with a token matching system
|
||||
- Clean up system playlist code
|
||||
|
||||
6.4.18:
|
||||
- Load each artist playlist in a separate idle task
|
||||
- UI spacing improvements
|
||||
|
||||
6.4.18-rc:
|
||||
- Remove stack widget from UI
|
||||
- Add Library Path playlists
|
||||
- Add Artist playlists
|
||||
|
||||
6.4.17:
|
||||
- Fix memory leak in __playlist_name()
|
||||
- Remember search text when changing displayed playlist
|
||||
|
||||
6.4.17-rc:
|
||||
- Filter can use GHashTables directly
|
||||
- Filter can store track pointers instead of indexes
|
||||
- Remove unused set and index classes
|
||||
- Convert Collection into a playlist
|
||||
- Convert History into a playlist
|
||||
|
||||
6.4.16:
|
||||
- Let tests run without fetching album art
|
||||
- Don't try to read data from nonexistant playlists
|
||||
- Show hidden folders when selecting new images
|
||||
- Add preview widget to album art chooser dialog
|
||||
- Fix blurry album art
|
||||
- Reduce time spent polling for album art
|
||||
- Split album art code into a new file
|
||||
- Fix PKGBUILD dependencies
|
||||
|
||||
6.4.16-rc:
|
||||
- Implement generic playlist layer
|
||||
- Implement separate queues for each playlist, including dynamic playlists
|
||||
- Remove core/containers/queue
|
||||
|
||||
6.4.15:
|
||||
- Add idle progress as a tooltip.
|
||||
- Initialize track labels with the correct initial size
|
||||
- Initialize album part with the correct size
|
||||
- Gui spacing updates
|
||||
|
||||
6.4.15-rc:
|
||||
- Write data files to a temporary file first, then rename
|
||||
- Add support for writing files to the user's XDG_USER_CACHE_DIR
|
||||
- Add idle tasks that run in a new thread
|
||||
- Fetch album art
|
||||
|
||||
6.4.14:
|
||||
- GtkPaned spacing improvements
|
||||
|
||||
6.4.14-rc:
|
||||
- Load files and databases using the idle queue
|
||||
- Switch over to using the glib g_random_int_range() function
|
||||
- Improve track average play count calculation
|
||||
|
||||
6.4.13:
|
||||
- Enable bolding the current track in the GtkTreeView
|
||||
- Enable GtkTreeView fixed-height mode
|
||||
- Fix bug where temporary queues don't save
|
||||
- Fix bug where a queue is not selected on startup
|
||||
- Fix bugs related to scrolling on track change
|
||||
|
||||
6.4.13-rc:
|
||||
- Rewrite GtkTreeView code
|
||||
- Save and Restore treeview column widths
|
||||
- Only have a single GtkTreeView instance
|
||||
- Finish conversion of Ocarina to C
|
||||
|
||||
6.4.12:
|
||||
- Don't reenable tempqueues when restarting
|
||||
- Control automatic pausing with a GtkComboBox
|
||||
- Move current position slider into the top section
|
||||
- Update tooltips on various buttons
|
||||
- Various UI tweaks
|
||||
- Swap position of random/repeat and favorite/hide buttons
|
||||
|
||||
6.4.12-rc:
|
||||
- Rewrite our custom GtkTreeModel and add tests
|
||||
- Move shuffle and repeat buttons into the sidebar
|
||||
|
||||
6.4.11:
|
||||
- Remove stop button
|
||||
- Move collection enabled checkboxes into a right click menu
|
||||
- Various UI tweaks (Thanks to Colin Fulton for helping here)
|
||||
|
||||
6.4.11-rc:
|
||||
- Convert window code to C
|
||||
- Convert collection gui code to C
|
||||
- Convert playlist gui code to C
|
||||
- Add sidebar and hide notebook tabs
|
||||
|
||||
6.4.10:
|
||||
- Block audio accelerator keys if a GtkEntry widget has focus
|
||||
- Various UI tweaks
|
||||
- Add changelog
|
||||
|
||||
6.4.10-rc:
|
||||
- Add generic settings functions
|
||||
- Convert code using GTK+ builder to C
|
||||
- Convert audio code to C
|
|
@ -0,0 +1,84 @@
|
|||
cmake_minimum_required(VERSION 3.4)
|
||||
project(Ocarina)
|
||||
|
||||
# Various options for users
|
||||
option(CONFIG_DEBUG "Compile with debugging symbols" OFF)
|
||||
option(CONFIG_TESTING_VERBOSE "Enable verbose output when running tests" OFF)
|
||||
option(CONFIG_TESTING_GUI "Enable running GUI tests (requires an X server)" ON)
|
||||
option(CONFIG_TESTING_ARTWORK "Enable album artwork fetching tests" ON)
|
||||
|
||||
# Configure settings
|
||||
set(CONFIG_MAJOR 6)
|
||||
set(CONFIG_MINOR 5)
|
||||
set(CONFIG_MICRO 10)
|
||||
set(CONFIG_RC ON)
|
||||
|
||||
set(CONFIG_VERSION "${CONFIG_MAJOR}.${CONFIG_MINOR}.${CONFIG_MICRO}")
|
||||
|
||||
if (CONFIG_RC)
|
||||
set(CONFIG_VERSION "${CONFIG_VERSION}-rc")
|
||||
endif()
|
||||
|
||||
set(DEBUG_OPTIONS -Wall -Werror -g -DCONFIG_DEBUG)
|
||||
if (CONFIG_DEBUG)
|
||||
add_definitions(${DEBUG_OPTIONS})
|
||||
set(CONFIG_VERSION "${CONFIG_VERSION}-debug")
|
||||
endif()
|
||||
|
||||
find_package(Git)
|
||||
if (GIT_FOUND AND IS_DIRECTORY .git/)
|
||||
configure_file(.git/HEAD .git/HEAD COPYONLY)
|
||||
execute_process(COMMAND ${GIT_EXECUTABLE} describe --abbrev=0
|
||||
COMMAND xargs ${GIT_EXECUTABLE} diff --quiet
|
||||
RESULT_VARIABLE HAS_CHANGED)
|
||||
if (HAS_CHANGED AND CONFIG_RC)
|
||||
set(CONFIG_VERSION "${CONFIG_VERSION}+")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_definitions(-DCONFIG_VERSION=\"${CONFIG_VERSION}\")
|
||||
add_definitions(-DCONFIG_MAJOR=${CONFIG_MAJOR})
|
||||
add_definitions(-DCONFIG_MINOR=${CONFIG_MINOR})
|
||||
|
||||
find_package(PkgConfig REQUIRED)
|
||||
function(use_module name module)
|
||||
pkg_check_modules(${name} REQUIRED ${module})
|
||||
include_directories(${${name}_INCLUDE_DIRS})
|
||||
add_definitions(${${name}_CFLAGS} ${${name}_CFLAGS_OTHERS})
|
||||
link_libraries(${${name}_LIBRARIES} ${${name}_LDFLAGS})
|
||||
endfunction()
|
||||
|
||||
use_module(GTK gtk+-3.0)
|
||||
use_module(GMODULE gmodule-export-2.0)
|
||||
use_module(GST gstreamer-1.0)
|
||||
use_module(TAGLIB taglib_c)
|
||||
use_module(CAA libcoverart)
|
||||
use_module(MB5 libmusicbrainz5)
|
||||
|
||||
# Tell compiler where to find header and source files
|
||||
include_directories(include)
|
||||
file(GLOB_RECURSE core "core/*.c")
|
||||
file(GLOB_RECURSE gui "gui/*.c")
|
||||
|
||||
# Configure executable
|
||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY bin/)
|
||||
add_executable(ocarina ${core} ${gui})
|
||||
|
||||
# Configure install targets
|
||||
install(TARGETS ocarina DESTINATION /usr/bin/)
|
||||
install(DIRECTORY share/ DESTINATION /usr/share/)
|
||||
|
||||
# Configure release target
|
||||
set(CONFIG_RELEASE ocarina-${CONFIG_VERSION})
|
||||
add_custom_target(release COMMAND git archive -v --prefix=${CONFIG_RELEASE}/ -o ${CONFIG_RELEASE}.tar.gz HEAD)
|
||||
|
||||
# Set up unit tests
|
||||
set(CMAKE_TEST_COMMAND ctest --output-on-failure)
|
||||
if (CONFIG_TESTING_VERBOSE)
|
||||
set(CMAKE_TEST_COMMAND ctest -V)
|
||||
endif()
|
||||
if (IS_DIRECTORY tests/)
|
||||
enable_testing()
|
||||
add_custom_target(tests COMMAND ${CMAKE_TEST_COMMAND})
|
||||
add_subdirectory(tests/)
|
||||
endif()
|
|
@ -0,0 +1 @@
|
|||
set(CTEST_CUSTOM_PRE_TEST "rm -rf $ENV{HOME}/.local/share/ocarina-test $ENV{HOME}/.cache/ocarina-test")
|
42
DESIGN
42
DESIGN
|
@ -1,42 +0,0 @@
|
|||
===============================================================================
|
||||
= =
|
||||
= Ocarina 6.3 =
|
||||
= =
|
||||
===============================================================================
|
||||
|
||||
Ocarina 6.2 is the 6th implementation of the Ocarina music player - a
|
||||
lightweight, GTK+ based music player with all the features that I want.
|
||||
Improvements over the 5.x series will include the existence of both a design
|
||||
document (this file) and unit tests. This should make maintenance easier and
|
||||
help me stay focused.
|
||||
|
||||
Ocarina 6.2 will use Gstreamer 1.0 for audio playback, GTK-MM 3 for user
|
||||
interface development, and Taglib for extracting tags.
|
||||
|
||||
|
||||
|
||||
Install:
|
||||
Ocarina will be compiled into a single executable placed under
|
||||
/usr/bin/. Any extra files needed to run will be placed under
|
||||
/usr/share/ocarina/.
|
||||
|
||||
|
||||
Core Layers:
|
||||
Ocarina was written with an "every file is a layer" strategy. This
|
||||
means code can only call functions that reside in a lower layer.
|
||||
From top to bottom, core layers are:
|
||||
|
||||
* Audio (include/core/audio.h)
|
||||
* Audio driver (include/core/driver.h)
|
||||
* Deck (include/core/deck.h)
|
||||
* Playlist (include/core/playlist.h)
|
||||
* Library (include/core/library.h)
|
||||
* Queue (include/core/queue.h)
|
||||
* RNG (include/core/random.h)
|
||||
* Tags (include/core/tags/*.h)
|
||||
* Idle queue (include/core/idle.h)
|
||||
* Filter (include/core/filter.h)
|
||||
* Index (include/core/index.h)
|
||||
* Databases (include/core/database.h)
|
||||
* File (include/core/file.h)
|
||||
* Version (include/core/version.h)
|
17
PKGBUILD
17
PKGBUILD
|
@ -1,23 +1,24 @@
|
|||
# Maintainer: Anna Schumaker <anna@ocarinaproject.net>
|
||||
# Maintainer: Anna Schumaker <anna@nowheycreamery.com>
|
||||
pkgname=ocarina
|
||||
pkgver=6.4.5
|
||||
pkgver=6.5.9
|
||||
pkgrel=1
|
||||
pkgdesc="A simple GTK and gstreamer based music player."
|
||||
url="http://www.ocarinaproject.net/"
|
||||
pkgdesc="A simple GTK+ and GStreamer based music player."
|
||||
url="http://www.nowheycreamery.com/"
|
||||
arch=('x86_64' 'i686' 'armv7h')
|
||||
license=('GPL2')
|
||||
depends=('gtkmm3>=3.16' 'gstreamer' 'gst-plugins-base' 'taglib')
|
||||
depends=('gtk3>=3.22' 'gstreamer' 'gst-plugins-base' 'taglib' 'libmusicbrainz5' 'libcoverart')
|
||||
optdepends=('gst-plugins-good' 'gst-plugins-bad' 'gst-plugins-ugly')
|
||||
makedepends=('scons')
|
||||
makedepends=('cmake')
|
||||
conflicts=()
|
||||
replaces=()
|
||||
backup=()
|
||||
source=("http://ocarinaproject.net/wp-content/ocarina/${pkgname}-${pkgver}.tar.gz")
|
||||
source=("http://nowheycreamery.com/wp-content/ocarina/${pkgname}-${pkgver}.tar.gz")
|
||||
sha1sums=('')
|
||||
|
||||
build() {
|
||||
cd "${srcdir}/${pkgname}-${pkgver}"
|
||||
scons $MAKEFLAGS
|
||||
cmake .
|
||||
make $MAKEFLAGS
|
||||
}
|
||||
|
||||
package() {
|
||||
|
|
17
README
17
README
|
@ -1,17 +0,0 @@
|
|||
Build:
|
||||
$ scons
|
||||
|
||||
Clean:
|
||||
$ scons -c
|
||||
|
||||
Install:
|
||||
$ sudo scons install
|
||||
|
||||
Uninstall:
|
||||
$ sudo scons -c install
|
||||
|
||||
Build tests:
|
||||
$ scons tests
|
||||
|
||||
Clean tests:
|
||||
$ scons -c tests
|
|
@ -0,0 +1,53 @@
|
|||
# Ocarina 6.5
|
||||
Ocarina is a simple GTK+ and GStreamer based music player written to let you listen to your music without getting in the way.
|
||||
|
||||
### Git
|
||||
Ocarina is tracked with Git, and can be cloned from the following sources:
|
||||
* http://git.nowheycreamery.com/anna/ocarina.git
|
||||
* git://git.nowheycreamery.com/anna/ocarina.git
|
||||
|
||||
##### Branches
|
||||
* [master]
|
||||
* The most recent release. This branch does not change frequently.
|
||||
* [next]
|
||||
* Changes that will be included in the next release. This branch is for testing and bugfixing.
|
||||
|
||||
### Building
|
||||
Ocarina uses `cmake`to control the build process. After cloning the code, run `cmake .` to generate the Makefile. Compile using `make`
|
||||
|
||||
##### CMake Options
|
||||
Ocarina supports the following options, which can be passed to `cmake` through `cmake -D<option>=<ON|OFF>`. For example, `cmake -DCONFIG_DEBUG=ON` would enable debugging.
|
||||
* CONFIG_DEBUG
|
||||
* Compile with debugging symbols enabled
|
||||
* CONFIG_TESTING_VERBOSE
|
||||
* Enable extra output when running unit tests
|
||||
* CONFIG_TESTING_GUI
|
||||
* Enable running GUI unit tests
|
||||
* CONFIG_TESTING_ARTWORK
|
||||
* Enable unit tests that fetch album artwork
|
||||
|
||||
### Running
|
||||
Once compiled, Ocarina can be run by invoking `bin/ocarina` on the command line.
|
||||
|
||||
##### Runtime options
|
||||
The following options can be passed to Ocarina during startup
|
||||
* -h, --help
|
||||
* Print help message and exit
|
||||
* -n, --next
|
||||
* Tell a running Ocarina instance to play the next track
|
||||
* - P, --pause
|
||||
* Tell a running Ocarina instance to pause playback
|
||||
* -p, --play
|
||||
* Tell a running Ocarina instance to start playback
|
||||
* -N, --previous
|
||||
* Tell a running Ocarina instance to play the previous track
|
||||
* -t, --toggle
|
||||
* Tell a paused Ocarina instance to begin playback, or tell a playing Ocarina instance to pause.
|
||||
* -v, --version
|
||||
* Print Ocarina version and exit
|
||||
|
||||
### Installing
|
||||
Running `make install` will install Ocarina for use by all users. There is currently no "uninstall" option.
|
||||
|
||||
### Testing
|
||||
Running `make tests` will compile and run the Ocarina unit tests. See the CMake Options section above for additional testing options.
|
57
Sconstruct
57
Sconstruct
|
@ -1,57 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
import os
|
||||
|
||||
CONFIG_VERSION = "6.4.5"
|
||||
|
||||
|
||||
# Set up default environment
|
||||
CONFIG_DEBUG = os.path.exists(".debug")
|
||||
CONFIG_CCFLAGS = [ "-O2" ]
|
||||
if CONFIG_DEBUG == True:
|
||||
CONFIG_CCFLAGS = [ "-Wall", "-Werror", "-g", "-DCONFIG_DEBUG" ]
|
||||
|
||||
class OEnvironment(Environment):
|
||||
def __init__(self, CCFLAGS = CONFIG_CCFLAGS):
|
||||
Environment.__init__(self, CCFLAGS = CCFLAGS)
|
||||
self.Append(CXXFLAGS = "-std=c++11");
|
||||
self.Append(CPPPATH = os.path.abspath("include"))
|
||||
self.Append(CXXCOMSTR = "C++ $TARGET")
|
||||
self.Append(CCCOMSTR = "CC $TARGET")
|
||||
self.Append(LINKCOMSTR = "Linking $TARGET")
|
||||
self.Debug = CONFIG_DEBUG
|
||||
self.Version = CONFIG_VERSION
|
||||
|
||||
def UsePackage(self, name):
|
||||
self.ParseConfig("pkg-config --cflags --libs %s" % name)
|
||||
|
||||
env = OEnvironment()
|
||||
Export("env")
|
||||
|
||||
|
||||
include = SConscript("include/Sconscript")
|
||||
core = SConscript("core/Sconscript")
|
||||
gui = SConscript("gui/Sconscript")
|
||||
|
||||
if os.path.isdir("tests"):
|
||||
tests = SConscript("tests/Sconscript")
|
||||
|
||||
|
||||
ocarina = env.Program("bin/ocarina", core + gui)
|
||||
Default(ocarina)
|
||||
Clean(ocarina, "bin/")
|
||||
|
||||
|
||||
def ocarina_release(target, source, env):
|
||||
o_vers = "ocarina-%s" % CONFIG_VERSION
|
||||
os.popen("git archive --prefix=%s/ -o %s.tar.gz HEAD" % (o_vers, o_vers))
|
||||
print os.popen("sha1sum %s.tar.gz" % o_vers).read()
|
||||
Command("release", None, ocarina_release)
|
||||
|
||||
|
||||
env.Install("/usr/bin", "bin/ocarina")
|
||||
env.Install("/usr/share", "share/ocarina/")
|
||||
env.Install("/usr/share/applications", "share/applications/ocarina.desktop")
|
||||
|
||||
install = Alias("install", [ "/usr/bin", "/usr/share", "/usr/share/applications" ])
|
||||
Depends(install, ocarina)
|
||||
Clean(install, "/usr/share/ocarina")
|
40
TODO
40
TODO
|
@ -4,33 +4,6 @@ Future work:
|
|||
to the wild. This section will be a list of features that I want, but
|
||||
should be deferred to a future release so basic support can be coded.
|
||||
|
||||
Hint: If feature B depends on A, implement A in 6.x and B in 6.x+1
|
||||
|
||||
- Categories:
|
||||
Use these to make "groups of groups" for better organization.
|
||||
Different categories can include Album, Artist and Genere
|
||||
dynamic groups in addition to user created groups (see below)
|
||||
|
||||
The Artist, Album and Genre "tables" can be used to populate
|
||||
these categories.
|
||||
|
||||
- User created song groups:
|
||||
Basic add and remove features can be implemented using the
|
||||
Library and Banned Songs groups. This will give me a chance
|
||||
to test saving groups on a small scale before letting users
|
||||
create anything they want.
|
||||
|
||||
- Save a user's playlist as a group:
|
||||
|
||||
- Library defragment:
|
||||
Ocarina 6.0 will leave holes in the library when tracks are
|
||||
deleted, potentially leading to fragmentation and larger-than-
|
||||
needed file sizes. A "defragment" utility can be created to
|
||||
clean up unused slots.
|
||||
|
||||
To help with fixing groups, a mapping of (old values) ->
|
||||
(new values) should be kept.
|
||||
|
||||
- Fix track durations:
|
||||
Some tracks in my library are tagged with the wrong duration,
|
||||
so fix them as they are played.
|
||||
|
@ -39,10 +12,6 @@ Future work:
|
|||
Make a pop-up window for editing the tags of a track. Be sure
|
||||
to update the library information and the on-disk file.
|
||||
|
||||
- Album art:
|
||||
(easy) Start with album art fetching script
|
||||
(hard) Build in to Ocarina
|
||||
|
||||
- Copy a song group to a different directory:
|
||||
This can be useful for keeping an external device (like an
|
||||
Android phone) updated with the music you want to listen to.
|
||||
|
@ -57,24 +26,15 @@ Future work:
|
|||
script, a cron job, and maybe a "mirror this track" option in
|
||||
the library? Perhaps create a mirror group?
|
||||
|
||||
- Preferences:
|
||||
- Set default sort
|
||||
- Save window size
|
||||
- Set album art size
|
||||
|
||||
- AirPlay / remote audio support
|
||||
|
||||
- Media keys
|
||||
|
||||
- Replaygain support
|
||||
External script to calculate values?
|
||||
Calculate value after first playback?
|
||||
Store in library :: Track structure
|
||||
|
||||
- "About" dialog
|
||||
- "On demand" databases that load the first time they are accessed?
|
||||
- Ports:
|
||||
- OSX
|
||||
- Windows
|
||||
- Android
|
||||
- Command line tools for web interface development?
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
res = Glob("*.cpp") + Glob("*.c")
|
||||
res += SConscript("tags/Sconscript")
|
||||
res += SConscript("containers/Sconscript")
|
||||
Return("res")
|
|
@ -0,0 +1,344 @@
|
|||
/*
|
||||
* Copyright 2013 (c) Anna Schumaker.
|
||||
*/
|
||||
#include <core/audio.h>
|
||||
#include <core/idle.h>
|
||||
#include <core/playlist.h>
|
||||
#include <core/settings.h>
|
||||
|
||||
#define LOAD_PLAYING (1 << 0) /* Begin playback after loading */
|
||||
#define LOAD_HISTORY (1 << 1) /* Add the track to the history */
|
||||
#define LOAD_DEFAULT (LOAD_PLAYING | LOAD_HISTORY)
|
||||
|
||||
static const char *SETTINGS_TRACK = "core.audio.cur";
|
||||
static const char *SETTINGS_VOLUME = "core.audio.volume";
|
||||
|
||||
static struct file audio_file = FILE_INIT_DATA("", "cur_track", 0);
|
||||
static struct track *audio_track = NULL;
|
||||
static int audio_pause_count = -1;
|
||||
|
||||
static GstElement *audio_pipeline = NULL;
|
||||
static GstElement *audio_source = NULL;
|
||||
static GstElement *audio_decoder = NULL;
|
||||
static GstElement *audio_converter = NULL;
|
||||
static GstElement *audio_volume = NULL;
|
||||
static GstElement *audio_sink = NULL;
|
||||
static guint audio_bus_id = 0;
|
||||
|
||||
static struct audio_callbacks *audio_cb = NULL;
|
||||
|
||||
|
||||
static bool __audio_change_state(GstState state)
|
||||
{
|
||||
if (audio_cur_state() == state)
|
||||
return false;
|
||||
return gst_element_set_state(audio_pipeline, state) != GST_STATE_CHANGE_FAILURE;
|
||||
}
|
||||
|
||||
static struct track *__audio_load(struct track *track, unsigned int flags)
|
||||
{
|
||||
struct track *prev = audio_track;
|
||||
gchar *path;
|
||||
|
||||
if (!track)
|
||||
return NULL;
|
||||
|
||||
audio_track = track;
|
||||
path = track_path(track);
|
||||
if (audio_cur_state() != GST_STATE_NULL)
|
||||
gst_element_set_state(audio_pipeline, GST_STATE_READY);
|
||||
g_object_set(G_OBJECT(audio_source), "location", path, NULL);
|
||||
gst_element_set_state(audio_pipeline, flags & LOAD_PLAYING ?
|
||||
GST_STATE_PLAYING : GST_STATE_PAUSED);
|
||||
|
||||
playlist_played(prev);
|
||||
if (prev && TRACK_IS_EXTERNAL(prev))
|
||||
track_free_external(prev);
|
||||
|
||||
playlist_selected(track);
|
||||
if (flags & LOAD_HISTORY && !TRACK_IS_EXTERNAL(track))
|
||||
playlist_add(playlist_lookup(PL_SYSTEM, "History"), track);
|
||||
if (audio_cb)
|
||||
audio_cb->audio_cb_load(track);
|
||||
|
||||
audio_save();
|
||||
g_free(path);
|
||||
return track;
|
||||
}
|
||||
|
||||
static void __audio_pad_added(GstElement *element, GstPad *pad, gpointer data)
|
||||
{
|
||||
GstPad *sink = gst_element_get_static_pad(audio_decoder, "sink");
|
||||
|
||||
gst_element_link(element, audio_converter);
|
||||
gst_pad_link(pad, sink);
|
||||
gst_object_unref(sink);
|
||||
}
|
||||
|
||||
static gboolean __audio_message(GstBus *bus, GstMessage *message, gpointer data)
|
||||
{
|
||||
GstObject *source = GST_OBJECT(GST_MESSAGE_SRC(message));
|
||||
gchar *debug = NULL;
|
||||
GError *error = NULL;
|
||||
GstState old, state, next;
|
||||
unsigned int load_flags = LOAD_DEFAULT;
|
||||
|
||||
switch (GST_MESSAGE_TYPE(message)) {
|
||||
case GST_MESSAGE_ERROR:
|
||||
gst_message_parse_error(message, &error, &debug);
|
||||
g_printerr("ERROR from element %s: %s\n",
|
||||
GST_OBJECT_NAME(source), error->message);
|
||||
g_printerr("DEBUG details: %s\n", debug ? debug : "none");
|
||||
g_error_free(error);
|
||||
g_free(debug);
|
||||
|
||||
if (audio_cur_state() != GST_STATE_PLAYING)
|
||||
load_flags = LOAD_HISTORY;
|
||||
__audio_load(playlist_next(), load_flags);
|
||||
break;
|
||||
|
||||
case GST_MESSAGE_EOS:
|
||||
track_played(audio_track);
|
||||
if (audio_pause_count >= 0) {
|
||||
audio_pause_after(audio_pause_count - 1);
|
||||
if (audio_pause_count == -1)
|
||||
load_flags = LOAD_HISTORY;
|
||||
}
|
||||
__audio_load(playlist_next(), load_flags);
|
||||
break;
|
||||
|
||||
case GST_MESSAGE_STATE_CHANGED:
|
||||
if (!audio_cb || source != GST_OBJECT(audio_pipeline))
|
||||
break;
|
||||
|
||||
gst_message_parse_state_changed(message, &old, &state, &next);
|
||||
if (state == GST_STATE_PLAYING || state == GST_STATE_PAUSED) {
|
||||
if (next == GST_STATE_VOID_PENDING)
|
||||
audio_cb->audio_cb_state_change(state);
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool __audio_init_idle(void *data)
|
||||
{
|
||||
unsigned int track;
|
||||
|
||||
if (settings_has(SETTINGS_TRACK)) {
|
||||
track = settings_get(SETTINGS_TRACK);
|
||||
__audio_load(track_get(track), LOAD_HISTORY);
|
||||
} else if (file_open(&audio_file, OPEN_READ)) {
|
||||
track = file_readu(&audio_file);
|
||||
file_close(&audio_file);
|
||||
file_remove(&audio_file);
|
||||
__audio_load(track_get(track), LOAD_HISTORY);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
void audio_init(int *argc, char ***argv, struct audio_callbacks *callbacks)
|
||||
{
|
||||
unsigned int volume = 100;
|
||||
GstBus *bus;
|
||||
|
||||
gst_init(argc, argv);
|
||||
audio_cb = callbacks;
|
||||
audio_pipeline = gst_pipeline_new("pipeline");
|
||||
audio_source = gst_element_factory_make("filesrc", "source");
|
||||
audio_decoder = gst_element_factory_make("decodebin", "decoder");
|
||||
audio_converter = gst_element_factory_make("audioconvert", "converter");
|
||||
audio_volume = gst_element_factory_make("volume", "volume");
|
||||
audio_sink = gst_element_factory_make("autoaudiosink", "sink");
|
||||
bus = gst_pipeline_get_bus(GST_PIPELINE(audio_pipeline));
|
||||
audio_bus_id = gst_bus_add_watch(bus, __audio_message, NULL);
|
||||
|
||||
gst_bin_add_many(GST_BIN(audio_pipeline), audio_source, audio_decoder,
|
||||
audio_converter, audio_volume,
|
||||
audio_sink, NULL);
|
||||
gst_element_link(audio_source, audio_decoder);
|
||||
gst_element_link_many(audio_converter, audio_volume, audio_sink, NULL);
|
||||
g_signal_connect(audio_decoder, "pad-added", G_CALLBACK(__audio_pad_added), NULL);
|
||||
gst_object_unref(bus);
|
||||
|
||||
if (settings_has(SETTINGS_VOLUME))
|
||||
volume = settings_get(SETTINGS_VOLUME);
|
||||
audio_set_volume(volume);
|
||||
|
||||
idle_schedule(IDLE_SYNC, __audio_init_idle, NULL);
|
||||
}
|
||||
|
||||
void audio_deinit()
|
||||
{
|
||||
gst_element_set_state(audio_pipeline, GST_STATE_NULL);
|
||||
gst_object_unref(GST_ELEMENT(audio_pipeline));
|
||||
g_source_remove(audio_bus_id);
|
||||
|
||||
audio_pipeline = NULL;
|
||||
audio_source = NULL;
|
||||
audio_decoder = NULL;
|
||||
audio_converter = NULL;
|
||||
audio_volume = NULL;
|
||||
audio_sink = NULL;
|
||||
audio_track = NULL;
|
||||
|
||||
gst_deinit();
|
||||
}
|
||||
|
||||
void audio_save()
|
||||
{
|
||||
if (audio_track && !TRACK_IS_EXTERNAL(audio_track))
|
||||
settings_set(SETTINGS_TRACK, track_index(audio_track));
|
||||
}
|
||||
|
||||
bool audio_load(struct track *track)
|
||||
{
|
||||
if (track == audio_track)
|
||||
return false;
|
||||
return __audio_load(track, LOAD_DEFAULT) != NULL;
|
||||
}
|
||||
|
||||
bool audio_load_filepath(const gchar *filepath)
|
||||
{
|
||||
struct track *track;
|
||||
|
||||
if (!filepath)
|
||||
return false;
|
||||
track = track_lookup(filepath);
|
||||
if (!track)
|
||||
track = track_alloc_external(filepath);
|
||||
return audio_load(track);
|
||||
}
|
||||
|
||||
struct track *audio_cur_track()
|
||||
{
|
||||
return audio_track;
|
||||
}
|
||||
|
||||
GstState audio_cur_state()
|
||||
{
|
||||
GstState cur = GST_STATE_NULL;
|
||||
if (audio_pipeline)
|
||||
gst_element_get_state(audio_pipeline,
|
||||
&cur, NULL,
|
||||
GST_CLOCK_TIME_NONE);
|
||||
return cur;
|
||||
}
|
||||
|
||||
void audio_set_volume(unsigned int volume)
|
||||
{
|
||||
gdouble vol;
|
||||
|
||||
if (volume > 100)
|
||||
volume = 100;
|
||||
vol = (gdouble)volume / 100;
|
||||
|
||||
settings_set(SETTINGS_VOLUME, volume);
|
||||
g_object_set(G_OBJECT(audio_volume), "volume", vol, NULL);
|
||||
}
|
||||
|
||||
unsigned int audio_get_volume()
|
||||
{
|
||||
gdouble volume;
|
||||
g_object_get(G_OBJECT(audio_volume), "volume", &volume, NULL);
|
||||
return volume * 100;
|
||||
}
|
||||
|
||||
bool audio_play()
|
||||
{
|
||||
if (!audio_track)
|
||||
return false;
|
||||
return __audio_change_state(GST_STATE_PLAYING);
|
||||
}
|
||||
|
||||
bool audio_pause()
|
||||
{
|
||||
if (!audio_track)
|
||||
return false;
|
||||
return __audio_change_state(GST_STATE_PAUSED);
|
||||
}
|
||||
|
||||
bool audio_seek(gint64 offset)
|
||||
{
|
||||
if (!audio_track)
|
||||
return false;
|
||||
return gst_element_seek_simple(audio_pipeline,
|
||||
GST_FORMAT_TIME,
|
||||
GST_SEEK_FLAG_FLUSH,
|
||||
offset);
|
||||
}
|
||||
|
||||
gint64 audio_position()
|
||||
{
|
||||
gint64 position;
|
||||
if (gst_element_query_position(audio_pipeline,
|
||||
GST_FORMAT_TIME,
|
||||
&position))
|
||||
return position;
|
||||
return 0;
|
||||
}
|
||||
|
||||
gint64 audio_duration()
|
||||
{
|
||||
gint64 duration;
|
||||
if (gst_element_query_duration(audio_pipeline,
|
||||
GST_FORMAT_TIME,
|
||||
&duration))
|
||||
return duration;
|
||||
if (audio_track)
|
||||
return audio_track->tr_length * GST_SECOND;
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct track *audio_next()
|
||||
{
|
||||
return __audio_load(playlist_next(), LOAD_DEFAULT);
|
||||
}
|
||||
|
||||
struct track *audio_prev()
|
||||
{
|
||||
return __audio_load(playlist_prev(), LOAD_PLAYING);
|
||||
}
|
||||
|
||||
bool audio_pause_after(int n)
|
||||
{
|
||||
if (n >= -1 && n != audio_pause_count) {
|
||||
audio_pause_count = n;
|
||||
if (audio_cb)
|
||||
audio_cb->audio_cb_config_pause(audio_pause_count);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int audio_get_pause_count(void)
|
||||
{
|
||||
return audio_pause_count;
|
||||
}
|
||||
|
||||
#ifdef CONFIG_TESTING
|
||||
void test_audio_eos()
|
||||
{
|
||||
GstMessage *message = gst_message_new_eos(GST_OBJECT(audio_pipeline));
|
||||
__audio_message(NULL, message, NULL);
|
||||
gst_message_unref(message);
|
||||
}
|
||||
|
||||
void test_audio_error(GError *error, gchar *debug)
|
||||
{
|
||||
GstMessage *message = gst_message_new_error(
|
||||
GST_OBJECT(audio_pipeline), error, debug);
|
||||
__audio_message(NULL, message, NULL);
|
||||
gst_message_unref(message);
|
||||
}
|
||||
|
||||
GstElement *test_audio_pipeline()
|
||||
{
|
||||
return audio_pipeline;
|
||||
}
|
||||
#endif /* CONFIG_TESTING */
|
175
core/audio.cpp
175
core/audio.cpp
|
@ -1,175 +0,0 @@
|
|||
/**
|
||||
* Copyright 2013 (c) Anna Schumaker.
|
||||
*/
|
||||
#include <core/audio.h>
|
||||
#include <core/deck.h>
|
||||
#include <core/library.h>
|
||||
#include <core/string.h>
|
||||
|
||||
|
||||
static bool _pause_enabled = false;
|
||||
static unsigned int _pause_count = 0;
|
||||
|
||||
static struct track *cur_track = NULL;
|
||||
static struct file f_cur_track;
|
||||
static AudioDriver *cur_driver = NULL;
|
||||
|
||||
|
||||
static void save_state()
|
||||
{
|
||||
file_open(&f_cur_track, OPEN_WRITE);
|
||||
file_writef(&f_cur_track, "%u\n", cur_track->tr_dbe.dbe_index);
|
||||
file_close(&f_cur_track);
|
||||
}
|
||||
|
||||
static void _load_track(struct track *track, bool start_playback)
|
||||
{
|
||||
cur_track = track;
|
||||
if (!track)
|
||||
return;
|
||||
|
||||
cur_driver->load(track);
|
||||
if (start_playback)
|
||||
audio :: play();
|
||||
else
|
||||
audio :: pause();
|
||||
save_state();
|
||||
}
|
||||
|
||||
static bool continue_playback()
|
||||
{
|
||||
bool ret = true;
|
||||
|
||||
if (_pause_enabled) {
|
||||
if (_pause_count == 0) {
|
||||
ret = false;
|
||||
_pause_enabled = false;
|
||||
} else
|
||||
_pause_count--;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
|
||||
AudioDriver :: AudioDriver()
|
||||
{
|
||||
cur_driver = this;
|
||||
}
|
||||
|
||||
AudioDriver :: ~AudioDriver()
|
||||
{
|
||||
cur_driver = NULL;
|
||||
}
|
||||
|
||||
void AudioDriver :: eos()
|
||||
{
|
||||
if (cur_track) {
|
||||
track_played(cur_track);
|
||||
queue_updated(collection :: get_queue(), cur_track);
|
||||
}
|
||||
|
||||
_load_track(deck :: next(), continue_playback());
|
||||
}
|
||||
|
||||
|
||||
|
||||
void audio :: init()
|
||||
{
|
||||
unsigned int id;
|
||||
|
||||
file_init(&f_cur_track, "cur_track", 0);
|
||||
if (file_exists(&f_cur_track)) {
|
||||
file_open(&f_cur_track, OPEN_READ);
|
||||
file_readf(&f_cur_track, "%u", &id);
|
||||
file_close(&f_cur_track);
|
||||
audio :: load_track(track_get(id));
|
||||
}
|
||||
}
|
||||
|
||||
void audio :: play()
|
||||
{
|
||||
if (cur_track)
|
||||
cur_driver->play();
|
||||
}
|
||||
|
||||
void audio :: pause()
|
||||
{
|
||||
if (cur_track)
|
||||
cur_driver->pause();
|
||||
}
|
||||
|
||||
void audio :: seek_to(int64_t pos)
|
||||
{
|
||||
if (cur_track)
|
||||
cur_driver->seek_to(pos);
|
||||
}
|
||||
|
||||
void audio :: stop()
|
||||
{
|
||||
pause();
|
||||
seek_to(0);
|
||||
}
|
||||
|
||||
int64_t audio :: position()
|
||||
{
|
||||
if (cur_track)
|
||||
return cur_driver->position();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int64_t audio :: duration()
|
||||
{
|
||||
if (cur_track)
|
||||
return cur_driver->duration();
|
||||
return 0;
|
||||
}
|
||||
|
||||
void audio :: next()
|
||||
{
|
||||
_load_track(deck :: next(), cur_driver->is_playing());
|
||||
}
|
||||
|
||||
void audio :: prev()
|
||||
{
|
||||
_load_track(deck :: prev(), cur_driver->is_playing());
|
||||
}
|
||||
|
||||
void audio :: load_track(struct track *track)
|
||||
{
|
||||
if (!track || track == cur_track)
|
||||
return;
|
||||
|
||||
_load_track(track, cur_driver->is_playing());
|
||||
queue_add(deck :: get_queue(), cur_track);
|
||||
}
|
||||
|
||||
struct track *audio :: current_track()
|
||||
{
|
||||
return cur_track;
|
||||
}
|
||||
|
||||
void audio :: pause_after(bool enabled, unsigned int n)
|
||||
{
|
||||
if (n > _pause_count)
|
||||
enabled = true;
|
||||
|
||||
_pause_enabled = enabled;
|
||||
_pause_count = n;
|
||||
}
|
||||
|
||||
bool audio :: pause_enabled()
|
||||
{
|
||||
return _pause_enabled;
|
||||
}
|
||||
|
||||
unsigned int audio :: pause_count()
|
||||
{
|
||||
return _pause_count;
|
||||
}
|
||||
|
||||
AudioDriver *audio :: get_driver()
|
||||
{
|
||||
return cur_driver;
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
res = Glob("*.c")
|
||||
Return("res")
|
|
@ -1,94 +0,0 @@
|
|||
/*
|
||||
* Copyright 2014 (c) Anna Schumaker.
|
||||
*/
|
||||
#include <core/containers/index.h>
|
||||
|
||||
|
||||
static struct index_entry *__index_alloc(gchar *key)
|
||||
{
|
||||
struct index_entry *ent = g_malloc(sizeof(struct index_entry));
|
||||
dbe_init(&ent->ie_dbe, ent);
|
||||
set_init(&ent->ie_set);
|
||||
ent->ie_key = key;
|
||||
return ent;
|
||||
}
|
||||
|
||||
static struct db_entry *index_alloc(const gchar *key)
|
||||
{
|
||||
return &__index_alloc(g_strdup(key))->ie_dbe;
|
||||
}
|
||||
|
||||
static void index_free(struct db_entry *dbe)
|
||||
{
|
||||
struct index_entry *ent = INDEX_ENTRY(dbe);
|
||||
set_deinit(&ent->ie_set);
|
||||
g_free(ent);
|
||||
}
|
||||
|
||||
static gchar *index_key(struct db_entry *dbe)
|
||||
{
|
||||
return INDEX_ENTRY(dbe)->ie_key;
|
||||
}
|
||||
|
||||
static void index_write(struct file *file, struct db_entry *dbe)
|
||||
{
|
||||
file_writef(file, "%s\n" , INDEX_ENTRY(dbe)->ie_key);
|
||||
set_write(file, &INDEX_ENTRY(dbe)->ie_set);
|
||||
}
|
||||
|
||||
static struct db_entry *index_read(struct file *file)
|
||||
{
|
||||
gchar *key = file_readl(file);
|
||||
struct index_entry *ent;
|
||||
|
||||
ent = __index_alloc(key);
|
||||
set_read(file, &ent->ie_set);
|
||||
return &ent->ie_dbe;
|
||||
}
|
||||
|
||||
|
||||
static const struct db_ops index_ops = {
|
||||
.dbe_alloc = index_alloc,
|
||||
.dbe_free = index_free,
|
||||
.dbe_key = index_key,
|
||||
.dbe_read = index_read,
|
||||
.dbe_write = index_write,
|
||||
};
|
||||
|
||||
|
||||
void index_init(struct database *index, const gchar *filepath, bool autosave)
|
||||
{
|
||||
db_init(index, filepath, autosave, &index_ops);
|
||||
}
|
||||
|
||||
struct index_entry *index_insert(struct database *index, const gchar *key,
|
||||
unsigned int value)
|
||||
{
|
||||
struct index_entry *it = INDEX_ENTRY(db_find(index, key));
|
||||
|
||||
set_insert(&it->ie_set, value);
|
||||
db_autosave(index);
|
||||
return it;
|
||||
}
|
||||
|
||||
void index_remove(struct database *index, const gchar *key, unsigned int value)
|
||||
{
|
||||
struct index_entry *it = INDEX_ENTRY(db_get(index, key));
|
||||
if (it) {
|
||||
set_remove(&it->ie_set, value);
|
||||
db_autosave(index);
|
||||
}
|
||||
}
|
||||
|
||||
bool index_has(struct database *index, const gchar *key, unsigned int value)
|
||||
{
|
||||
struct index_entry *it = INDEX_ENTRY(db_get(index, key));
|
||||
|
||||
if (!it)
|
||||
return false;
|
||||
return set_has(&it->ie_set, value);
|
||||
}
|
||||
|
||||
#ifdef CONFIG_TESTING
|
||||
const struct db_ops *test_index_ops() { return &index_ops; }
|
||||
#endif /* CONFIG_TESTING */
|
|
@ -1,29 +0,0 @@
|
|||
/*
|
||||
* Copyright 2015 (c) Anna Schumaker.
|
||||
*/
|
||||
|
||||
#include <core/containers/queue.h>
|
||||
|
||||
guint _q_add_sorted(struct _queue *queue, gpointer data,
|
||||
GCompareDataFunc func, gpointer user_data)
|
||||
{
|
||||
struct _q_iter it;
|
||||
|
||||
_q_for_each(queue, &it) {
|
||||
if (func(_q_iter_val(&it), data, user_data) > 0) {
|
||||
g_queue_insert_before(&queue->_queue, it.it_iter, data);
|
||||
return it.it_pos;
|
||||
}
|
||||
}
|
||||
return _q_add_tail(queue, data);
|
||||
}
|
||||
|
||||
gpointer _q_remove_it(struct _queue *queue, struct _q_iter *it)
|
||||
{
|
||||
gpointer ret = _q_iter_val(it);
|
||||
GList *link = it->it_iter;
|
||||
|
||||
_q_iter_prev(it);
|
||||
g_queue_delete_link(&queue->_queue, link);
|
||||
return ret;
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
/*
|
||||
* Copyright 2015 (c) Anna Schumaker.
|
||||
*/
|
||||
|
||||
#include <core/containers/set.h>
|
||||
|
||||
void set_copy(const struct set *lhs, struct set *rhs)
|
||||
{
|
||||
struct set_iter it_lhs;
|
||||
|
||||
set_for_each(lhs, &it_lhs)
|
||||
set_insert(rhs, it_lhs.it_val);
|
||||
}
|
||||
|
||||
void set_inline_intersect(const struct set *lhs, struct set *rhs)
|
||||
{
|
||||
struct set_iter it_rhs;
|
||||
|
||||
set_for_each(rhs, &it_rhs) {
|
||||
if (!set_has(lhs, it_rhs.it_val))
|
||||
g_hash_table_iter_remove(&it_rhs.it_iter);
|
||||
}
|
||||
}
|
||||
|
||||
void set_read(struct file *file, struct set *set)
|
||||
{
|
||||
unsigned int num, val, i;
|
||||
|
||||
file_readf(file, "%u ", &num);
|
||||
for (i = 0; i < num; i++) {
|
||||
file_readf(file, "%u ", &val);
|
||||
set_insert(set, val);
|
||||
}
|
||||
}
|
||||
|
||||
void set_write(struct file *file, struct set *set)
|
||||
{
|
||||
struct set_iter it;
|
||||
|
||||
file_writef(file, "%u ", set_size(set));
|
||||
set_for_each(set, &it)
|
||||
file_writef(file, "%u ", it.it_val);
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright 2014 (c) Anna Schumaker.
|
||||
*/
|
||||
#include <core/core.h>
|
||||
|
||||
static bool core_defragment(void *data)
|
||||
{
|
||||
if (tags_defragment()) {
|
||||
playlist_save();
|
||||
audio_save();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void core_init(int *argc, char ***argv, struct playlist_callbacks *playlist_cb,
|
||||
struct audio_callbacks *audio_cb, enum idle_sync_t idle_sync)
|
||||
{
|
||||
idle_init(idle_sync);
|
||||
settings_init();
|
||||
tags_init();
|
||||
playlist_init(playlist_cb);
|
||||
audio_init(argc, argv, audio_cb);
|
||||
|
||||
idle_schedule(IDLE_SYNC, core_defragment, NULL);
|
||||
}
|
||||
|
||||
void core_deinit()
|
||||
{
|
||||
audio_deinit();
|
||||
playlist_deinit();
|
||||
tags_deinit();
|
||||
settings_deinit();
|
||||
idle_deinit();
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
/**
|
||||
* Copyright 2014 (c) Anna Schumaker.
|
||||
*/
|
||||
#include <core/audio.h>
|
||||
#include <core/core.h>
|
||||
#include <core/deck.h>
|
||||
extern "C" {
|
||||
#include <core/filter.h>
|
||||
#include <core/tags/tags.h>
|
||||
}
|
||||
#include <core/library.h>
|
||||
#include <core/playlist.h>
|
||||
|
||||
|
||||
void core :: init(struct core_init_data *init)
|
||||
{
|
||||
filter_init();
|
||||
tags_init();
|
||||
collection :: init(init->collection_ops);
|
||||
playlist :: init(init->playlist_ops);
|
||||
deck :: init(init->history_ops, init->tempq_ops);
|
||||
audio :: init();
|
||||
}
|
||||
|
||||
void core :: deinit()
|
||||
{
|
||||
filter_deinit();
|
||||
tags_deinit();
|
||||
}
|
|
@ -1,7 +1,8 @@
|
|||
/*
|
||||
* Copyright 2013 (c) Anna Schumaker.
|
||||
*/
|
||||
#include <core/containers/database.h>
|
||||
#include <core/database.h>
|
||||
#include <core/idle.h>
|
||||
|
||||
#define DB_ENTRY_AT(db, index) \
|
||||
(struct db_entry *)g_ptr_array_index(db->db_entries, index)
|
||||
|
@ -35,11 +36,9 @@ static struct db_entry *__dbe_next(const struct database *db, unsigned int index
|
|||
static struct db_entry *__dbe_read(struct database *db, unsigned int index)
|
||||
{
|
||||
struct db_entry *dbe = NULL;
|
||||
int valid;
|
||||
|
||||
file_readf(&db->db_file, "%d", &valid);
|
||||
if (valid)
|
||||
dbe = db->db_ops->dbe_read(&db->db_file);
|
||||
if (file_readd(&db->db_file))
|
||||
dbe = db->db_ops->dbe_read(&db->db_file, index);
|
||||
|
||||
g_ptr_array_index(db->db_entries, index) = dbe;
|
||||
return dbe;
|
||||
|
@ -54,8 +53,6 @@ static void __dbe_setup(struct database *db, unsigned int index)
|
|||
dbe->dbe_key = db->db_ops->dbe_key(dbe);
|
||||
g_hash_table_insert(db->db_keys, dbe->dbe_key, dbe);
|
||||
db->db_size++;
|
||||
if (db->db_ops->dbe_setup)
|
||||
db->db_ops->dbe_setup(dbe);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -70,14 +67,14 @@ static void __dbe_write(struct database *db, struct db_entry *dbe)
|
|||
}
|
||||
|
||||
void db_init(struct database *db, const char *filepath, bool autosave,
|
||||
const struct db_ops *ops)
|
||||
const struct db_ops *ops, unsigned int fmin)
|
||||
{
|
||||
db->db_ops = ops;
|
||||
db->db_size = 0;
|
||||
db->db_autosave = autosave;
|
||||
db->db_entries = g_ptr_array_new();
|
||||
db->db_keys = g_hash_table_new(g_str_hash, g_str_equal);
|
||||
file_init(&db->db_file, filepath, 0);
|
||||
file_init_data(&db->db_file, "", filepath, fmin);
|
||||
}
|
||||
|
||||
void db_deinit(struct database *db)
|
||||
|
@ -114,18 +111,23 @@ void db_autosave(struct database *db)
|
|||
void db_load(struct database *db)
|
||||
{
|
||||
unsigned int size;
|
||||
bool save;
|
||||
|
||||
if (file_open(&db->db_file, OPEN_READ) == false)
|
||||
return;
|
||||
|
||||
file_readf(&db->db_file, "%u", &size);
|
||||
size = file_readu(&db->db_file);
|
||||
g_ptr_array_set_size(db->db_entries, size);
|
||||
for (unsigned int i = 0; i < size; i++) {
|
||||
if (__dbe_read(db, i))
|
||||
__dbe_setup(db, i);
|
||||
}
|
||||
|
||||
save = file_version(&db->db_file) != OCARINA_MINOR_VERSION;
|
||||
file_close(&db->db_file);
|
||||
|
||||
if (save)
|
||||
db_autosave(db);
|
||||
}
|
||||
|
||||
struct db_entry *db_insert(struct database *db, const gchar *key)
|
||||
|
@ -133,7 +135,7 @@ struct db_entry *db_insert(struct database *db, const gchar *key)
|
|||
struct db_entry *item = NULL;
|
||||
|
||||
if (key)
|
||||
item = db->db_ops->dbe_alloc(key);
|
||||
item = db->db_ops->dbe_alloc(key, db_actual_size(db));
|
||||
|
||||
if (item) {
|
||||
g_ptr_array_add(db->db_entries, item);
|
||||
|
@ -153,6 +155,43 @@ void db_remove(struct database *db, struct db_entry *item)
|
|||
db_autosave(db);
|
||||
}
|
||||
|
||||
bool db_defrag(struct database *db)
|
||||
{
|
||||
struct db_entry *dbe;
|
||||
unsigned int i, cur;
|
||||
|
||||
if (db->db_size == db_actual_size(db))
|
||||
return false;
|
||||
|
||||
for (cur = 0, i = 1; cur < db->db_size; cur++) {
|
||||
if (DB_ENTRY_AT(db, cur))
|
||||
continue;
|
||||
while (i <= cur || !DB_ENTRY_AT(db, i))
|
||||
i++;
|
||||
|
||||
dbe = DB_ENTRY_AT(db, i);
|
||||
g_ptr_array_index(db->db_entries, i) = NULL;
|
||||
g_ptr_array_index(db->db_entries, cur) = dbe;
|
||||
dbe->dbe_index = cur;
|
||||
}
|
||||
|
||||
g_ptr_array_set_size(db->db_entries, db->db_size);
|
||||
db_autosave(db);
|
||||
return true;
|
||||
}
|
||||
|
||||
void db_rekey(struct database *db, struct db_entry *dbe)
|
||||
{
|
||||
if (dbe == NULL)
|
||||
return;
|
||||
|
||||
g_hash_table_remove(db->db_keys, dbe->dbe_key);
|
||||
g_free(dbe->dbe_key);
|
||||
|
||||
dbe->dbe_key = db->db_ops->dbe_key(dbe);
|
||||
g_hash_table_insert(db->db_keys, dbe->dbe_key, dbe);
|
||||
}
|
||||
|
||||
unsigned int db_actual_size(const struct database *db)
|
||||
{
|
||||
if (db->db_entries)
|
||||
|
@ -172,7 +211,7 @@ struct db_entry *db_next(const struct database *db, struct db_entry *ent)
|
|||
return NULL;
|
||||
}
|
||||
|
||||
struct db_entry *db_at(struct database *db, unsigned int index)
|
||||
struct db_entry *db_at(const struct database *db, unsigned int index)
|
||||
{
|
||||
if (index >= db_actual_size(db))
|
||||
return NULL;
|
15
core/date.c
15
core/date.c
|
@ -3,6 +3,7 @@
|
|||
*/
|
||||
#include <core/date.h>
|
||||
#include <core/string.h>
|
||||
#include <endian.h>
|
||||
#include <time.h>
|
||||
|
||||
|
||||
|
@ -25,7 +26,14 @@ void date_today(struct date *date)
|
|||
|
||||
void date_read(struct file *f, struct date *date)
|
||||
{
|
||||
file_readf(f, "%u %u %u", &date->d_year, &date->d_month, &date->d_day);
|
||||
date->d_year = file_readu(f);
|
||||
date->d_month = file_readu(f);
|
||||
date->d_day = file_readu(f);
|
||||
}
|
||||
|
||||
void date_read_stamp(struct file *f, struct date *date)
|
||||
{
|
||||
date->d_stamp = be32toh(file_readu(f));
|
||||
}
|
||||
|
||||
void date_write(struct file *f, struct date *date)
|
||||
|
@ -33,6 +41,11 @@ void date_write(struct file *f, struct date *date)
|
|||
file_writef(f, "%u %u %u", date->d_year, date->d_month, date->d_day);
|
||||
}
|
||||
|
||||
void date_write_stamp(struct file *f, struct date *date)
|
||||
{
|
||||
file_writef(f, "%u", htobe32(date->d_stamp));
|
||||
}
|
||||
|
||||
gchar *date_string(const struct date *date)
|
||||
{
|
||||
struct tm tm = {
|
||||
|
|
241
core/deck.cpp
241
core/deck.cpp
|
@ -1,241 +0,0 @@
|
|||
/**
|
||||
* Copyright 2013 (c) Anna Schumaker.
|
||||
*/
|
||||
#include <core/deck.h>
|
||||
#include <core/file.h>
|
||||
#include <core/library.h>
|
||||
|
||||
|
||||
static std::list<TempQueue> queue_deck;
|
||||
static struct queue recent_queue;
|
||||
static struct file deck_file;
|
||||
|
||||
|
||||
TempQueue :: TempQueue()
|
||||
{
|
||||
queue_init(this, 0, NULL);
|
||||
}
|
||||
|
||||
TempQueue :: TempQueue(bool random, struct queue_ops *ops)
|
||||
{
|
||||
unsigned int flags = Q_ENABLED | Q_SAVE_FLAGS | Q_SAVE_SORT;
|
||||
queue_init(this, flags | (random ? Q_RANDOM : 0), ops);
|
||||
}
|
||||
|
||||
|
||||
void TempQueue :: write(file &file)
|
||||
{
|
||||
file_writef(&file, "%u %zu", q_flags, queue_size(this));
|
||||
for (unsigned int i = 0; i < queue_size(this); i++)
|
||||
file_writef(&file, " %u", queue_at(this, i)->tr_dbe.dbe_index);
|
||||
}
|
||||
|
||||
void TempQueue :: read(file &file)
|
||||
{
|
||||
unsigned int n, id;
|
||||
file_readf(&file, "%u %u", &q_flags, &n);
|
||||
for (unsigned int i = 0; i < n; i++) {
|
||||
file_readf(&file, "%u", &id);
|
||||
queue_add(this, track_get(id));
|
||||
}
|
||||
}
|
||||
|
||||
unsigned int TempQueue :: add(struct track *track)
|
||||
{
|
||||
unsigned int res = queue_add(this, track);
|
||||
deck :: write();
|
||||
return res;
|
||||
}
|
||||
|
||||
void TempQueue :: del(struct track *track)
|
||||
{
|
||||
queue_remove_all(this, track);
|
||||
deck :: write();
|
||||
}
|
||||
|
||||
void TempQueue :: del(unsigned int id)
|
||||
{
|
||||
queue_remove(this, id);
|
||||
deck :: write();
|
||||
}
|
||||
|
||||
|
||||
static void upgrade_v0()
|
||||
{
|
||||
int random, ascending;
|
||||
unsigned int num, field;
|
||||
queue *library = collection :: get_queue();
|
||||
|
||||
file_readf(&deck_file, "%d %u", &random, &num);
|
||||
if (random)
|
||||
queue_set_flag(library, Q_RANDOM);
|
||||
|
||||
for (unsigned int i = 0; i < num; i++) {
|
||||
file_readf(&deck_file, "%u %d", &field, &ascending);
|
||||
queue_sort(library, (compare_t)field, (i == 0) ? true : false);
|
||||
if (!ascending)
|
||||
queue_sort(library, (compare_t)field, false);
|
||||
}
|
||||
}
|
||||
|
||||
void deck :: init(struct queue_ops *history_ops, struct queue_ops *temp_ops)
|
||||
{
|
||||
unsigned int num;
|
||||
bool upgraded = false;
|
||||
std::list<TempQueue>::iterator it;
|
||||
|
||||
queue_init(&recent_queue, Q_ENABLED | Q_REPEAT | Q_NO_SORT | Q_ADD_FRONT, history_ops);
|
||||
file_init(&deck_file, "deck", 1);
|
||||
|
||||
if (!file_open(&deck_file, OPEN_READ))
|
||||
return;
|
||||
|
||||
if (file_version(&deck_file) == 0) {
|
||||
upgrade_v0();
|
||||
upgraded = true;
|
||||
}
|
||||
|
||||
file_readf(&deck_file, "%u", &num);
|
||||
queue_deck.resize(num);
|
||||
|
||||
for (it = queue_deck.begin(); it != queue_deck.end(); it++) {
|
||||
it->read(deck_file);
|
||||
it->q_ops = temp_ops;
|
||||
}
|
||||
file_close(&deck_file);
|
||||
|
||||
if (upgraded)
|
||||
deck :: write();
|
||||
}
|
||||
|
||||
void deck :: write()
|
||||
{
|
||||
std::list<TempQueue>::iterator it;
|
||||
|
||||
if (!file_open(&deck_file, OPEN_WRITE))
|
||||
return;
|
||||
|
||||
file_writef(&deck_file, "%zu\n", queue_deck.size());
|
||||
for (it = queue_deck.begin(); it != queue_deck.end(); it++) {
|
||||
it->write(deck_file);
|
||||
file_writef(&deck_file, "\n");
|
||||
}
|
||||
|
||||
file_close(&deck_file);
|
||||
}
|
||||
|
||||
void deck :: save(struct queue *queue, enum queue_flags flag)
|
||||
{
|
||||
deck :: write();
|
||||
}
|
||||
|
||||
queue *deck :: create(bool random, struct queue_ops *ops)
|
||||
{
|
||||
queue_deck.push_back(TempQueue(random, ops));
|
||||
return &queue_deck.back();
|
||||
}
|
||||
|
||||
static void _destroy(std::list<TempQueue>::iterator &it)
|
||||
{
|
||||
queue_deck.erase(it);
|
||||
deck :: write();
|
||||
}
|
||||
|
||||
void deck :: destroy(queue *queue)
|
||||
{
|
||||
std::list<TempQueue>::iterator it;
|
||||
|
||||
for (it = queue_deck.begin(); it != queue_deck.end(); it++) {
|
||||
if (&(*it) == queue) {
|
||||
_destroy(it);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void deck :: move(queue *queue, unsigned int index)
|
||||
{
|
||||
unsigned int old_pos = deck :: index(queue);
|
||||
std::list<TempQueue>::iterator it_old = queue_deck.begin();
|
||||
std::list<TempQueue>::iterator it_new = queue_deck.begin();
|
||||
|
||||
for (unsigned int i = 0; i < queue_deck.size(); i++) {
|
||||
if (i < old_pos)
|
||||
it_old++;
|
||||
|
||||
if (i < index)
|
||||
it_new++;
|
||||
}
|
||||
|
||||
if (index > old_pos)
|
||||
it_new++;
|
||||
|
||||
queue_deck.splice(it_new, queue_deck, it_old);
|
||||
write();
|
||||
}
|
||||
|
||||
unsigned int deck :: index(queue *queue)
|
||||
{
|
||||
unsigned int i = 0;
|
||||
std::list<TempQueue>::iterator it;
|
||||
|
||||
for (it = queue_deck.begin(); it != queue_deck.end(); it++) {
|
||||
if (&(*it) == queue)
|
||||
return i;
|
||||
i++;
|
||||
}
|
||||
|
||||
return queue_deck.size();
|
||||
}
|
||||
|
||||
queue *deck :: get(unsigned int index)
|
||||
{
|
||||
std::list<TempQueue>::iterator it;
|
||||
|
||||
for (it = queue_deck.begin(); it != queue_deck.end(); it++) {
|
||||
if (index == 0)
|
||||
return &(*it);
|
||||
index--;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
struct track *deck :: next()
|
||||
{
|
||||
struct track *track = NULL;
|
||||
std::list<TempQueue>::iterator it;
|
||||
|
||||
for (it = queue_deck.begin(); it != queue_deck.end(); it++) {
|
||||
if (queue_has_flag(&(*it), Q_ENABLED) == false)
|
||||
continue;
|
||||
|
||||
track = queue_next(&(*it));
|
||||
if (queue_size(&(*it)) == 0)
|
||||
_destroy(it);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!track)
|
||||
track = queue_next(collection :: get_queue());
|
||||
if (track) {
|
||||
queue_remove_all(&recent_queue, track);
|
||||
queue_add(&recent_queue, track);
|
||||
_q_iter_set(&recent_queue.q_tracks, &recent_queue.q_cur, 0);
|
||||
}
|
||||
return track;
|
||||
}
|
||||
|
||||
struct track *deck :: prev()
|
||||
{
|
||||
return queue_next(&recent_queue);
|
||||
}
|
||||
|
||||
std::list<TempQueue> &deck :: get_queues()
|
||||
{
|
||||
return queue_deck;
|
||||
}
|
||||
|
||||
queue *deck :: get_queue()
|
||||
{
|
||||
return &recent_queue;
|
||||
}
|
267
core/file.c
267
core/file.c
|
@ -3,51 +3,105 @@
|
|||
*/
|
||||
#include <core/file.h>
|
||||
#include <core/string.h>
|
||||
#include <core/version.h>
|
||||
#include <errno.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifdef CONFIG_TESTING
|
||||
const gchar *OCARINA_DIR = "ocarina-test";
|
||||
#elif CONFIG_DEBUG
|
||||
const gchar *OCARINA_DIR = "ocarina-debug";
|
||||
#else
|
||||
const gchar *OCARINA_DIR = "ocarina";
|
||||
#endif
|
||||
#define REPORT_ERROR(fname, error) \
|
||||
g_printerr("%s (%s:%d): %s: %s\n", __func__, __FILE__, __LINE__, fname, error)
|
||||
#define REPORT_ERRNO(fname) REPORT_ERROR(fname, strerror(errno))
|
||||
|
||||
#define REPORT_ERROR(fname) \
|
||||
printf("%s (%s:%d): %s: %s\n", __func__, __FILE__, __LINE__, fname, strerror(errno))
|
||||
|
||||
|
||||
static gchar *__file_path(gchar *name)
|
||||
static void __file_init_common(struct file *file, const gchar *subdir,
|
||||
const gchar *name, unsigned int min,
|
||||
const gchar *(*user_dir)(void))
|
||||
{
|
||||
return g_strjoin("/", g_get_user_data_dir(), OCARINA_DIR, name, NULL);
|
||||
file->f_file = NULL;
|
||||
file->f_name = name;
|
||||
file->f_subdir = subdir;
|
||||
file->f_mode = CLOSED;
|
||||
file->f_version = OCARINA_MINOR_VERSION;
|
||||
file->f_prev = 0;
|
||||
file->f_min = min;
|
||||
file->f_user_dir = user_dir;
|
||||
}
|
||||
|
||||
static bool __file_mkdir()
|
||||
static bool __file_open(struct file *file, enum open_mode mode)
|
||||
{
|
||||
gchar *dir = __file_path(NULL);
|
||||
gchar *cmode, *path;
|
||||
|
||||
if (mode == OPEN_READ || mode == OPEN_READ_BINARY) {
|
||||
cmode = "r";
|
||||
path = file_path(file);
|
||||
} else {
|
||||
cmode = "w";
|
||||
path = file_write_path(file);
|
||||
}
|
||||
|
||||
file->f_file = g_fopen(path, cmode);
|
||||
if (!file->f_file)
|
||||
REPORT_ERRNO(path);
|
||||
g_free(path);
|
||||
return file->f_file != NULL;
|
||||
}
|
||||
|
||||
static bool __file_mkdir(struct file *file)
|
||||
{
|
||||
gchar *dir = g_build_filename(file->f_user_dir(), OCARINA_NAME,
|
||||
file->f_subdir, NULL);
|
||||
int ret = g_mkdir_with_parents(dir, 0755);
|
||||
|
||||
g_free(dir);
|
||||
if (ret != 0)
|
||||
REPORT_ERROR(dir);
|
||||
REPORT_ERRNO(dir);
|
||||
g_free(dir);
|
||||
return ret == 0;
|
||||
}
|
||||
|
||||
|
||||
void file_init(struct file *file, const gchar *name, unsigned int version)
|
||||
static bool __file_can_write(struct file *file)
|
||||
{
|
||||
file->f_mode = OPEN_READ;
|
||||
file->f_version = version;
|
||||
file->f_prev = 0;
|
||||
file->f_file = NULL;
|
||||
g_strlcpy(file->f_name, (name == NULL) ? "" : name, FILE_MAX_LEN);
|
||||
gchar *path = file_path(file);
|
||||
bool ret = true;
|
||||
|
||||
if (g_access(path, F_OK) == 0 && g_access(path, W_OK) < 0)
|
||||
ret = false;
|
||||
|
||||
g_free(path);
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
void file_init_data(struct file *file, const gchar *subdir,
|
||||
const gchar *name, unsigned int min)
|
||||
{
|
||||
__file_init_common(file, subdir, name, min, g_get_user_data_dir);
|
||||
}
|
||||
|
||||
void file_init_cache(struct file *file, const gchar *subdir, const gchar *name)
|
||||
{
|
||||
__file_init_common(file, subdir, name, 0, g_get_user_cache_dir);
|
||||
}
|
||||
|
||||
gchar *file_path(struct file *file)
|
||||
{
|
||||
if (strlen(file->f_name) != 0)
|
||||
return __file_path(file->f_name);
|
||||
return g_strdup("");
|
||||
if (string_length(file->f_name) == 0)
|
||||
return g_strdup("");
|
||||
return g_build_filename(file->f_user_dir(), OCARINA_NAME,
|
||||
file->f_subdir, file->f_name, NULL);
|
||||
}
|
||||
|
||||
gchar *file_write_path(struct file *file)
|
||||
{
|
||||
gchar *tmp, *res;
|
||||
|
||||
if (string_length(file->f_name) == 0)
|
||||
return g_strdup("");
|
||||
|
||||
tmp = g_strdup_printf(".%s.tmp", file->f_name);
|
||||
res = g_build_filename(file->f_user_dir(), OCARINA_NAME,
|
||||
file->f_subdir, tmp, NULL);
|
||||
g_free(tmp);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
const unsigned int file_version(struct file *file)
|
||||
|
@ -60,83 +114,98 @@ const unsigned int file_version(struct file *file)
|
|||
bool file_exists(struct file *file)
|
||||
{
|
||||
gchar *path = file_path(file);
|
||||
bool ret = g_file_test(path, G_FILE_TEST_EXISTS);
|
||||
|
||||
bool ret = g_file_test(path, G_FILE_TEST_EXISTS);
|
||||
g_free(path);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static bool __file_open_common(struct file *file, enum open_mode mode)
|
||||
{
|
||||
gchar *path = file_path(file);
|
||||
file->f_file = g_fopen(path, (mode == OPEN_READ) ? "r" : "w");
|
||||
g_free(path);
|
||||
|
||||
if (!file->f_file) {
|
||||
REPORT_ERROR(file->f_name);
|
||||
return false;
|
||||
}
|
||||
|
||||
file->f_mode = mode;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool __file_open_read(struct file *file)
|
||||
static bool __file_open_read(struct file *file, enum open_mode mode)
|
||||
{
|
||||
if (!file_exists(file))
|
||||
return false;
|
||||
if (!__file_open_common(file, OPEN_READ))
|
||||
if (!__file_open(file, mode))
|
||||
return false;
|
||||
return file_readf(file, "%u\n", &file->f_prev) == 1;
|
||||
|
||||
file->f_mode = mode;
|
||||
if (mode == OPEN_READ_BINARY)
|
||||
return true;
|
||||
|
||||
file->f_prev = file_readu(file);
|
||||
if (file->f_prev < file->f_min) {
|
||||
REPORT_ERROR(file->f_name, "File too old to be upgraded.");
|
||||
file_close(file);
|
||||
exit(1);
|
||||
}
|
||||
if (file->f_prev > file->f_version) {
|
||||
REPORT_ERROR(file->f_name, "File too new to be opened.");
|
||||
file_close(file);
|
||||
exit(1);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool __file_open_write(struct file *file)
|
||||
static bool __file_open_write(struct file *file, enum open_mode mode)
|
||||
{
|
||||
if (!__file_mkdir())
|
||||
if (!__file_mkdir(file))
|
||||
return false;
|
||||
if (!__file_open_common(file, OPEN_WRITE))
|
||||
if (!__file_can_write(file))
|
||||
return false;
|
||||
if (!__file_open(file, OPEN_WRITE))
|
||||
return false;
|
||||
|
||||
file->f_mode = mode;
|
||||
if (mode == OPEN_WRITE_BINARY)
|
||||
return true;
|
||||
return file_writef(file, "%d\n", file->f_version) > 0;
|
||||
}
|
||||
|
||||
bool file_open(struct file *file, enum open_mode mode)
|
||||
{
|
||||
if ((strlen(file->f_name) == 0) || (file->f_file != NULL))
|
||||
if ((string_length(file->f_name) == 0) || (file->f_file != NULL))
|
||||
return false;
|
||||
if (mode == CLOSED)
|
||||
return false;
|
||||
|
||||
if (mode == OPEN_READ)
|
||||
return __file_open_read(file);
|
||||
return __file_open_write(file);
|
||||
if (mode == OPEN_READ || mode == OPEN_READ_BINARY)
|
||||
return __file_open_read(file, mode);
|
||||
return __file_open_write(file, mode);
|
||||
}
|
||||
|
||||
void file_close(struct file *file)
|
||||
{
|
||||
if (file->f_file)
|
||||
gchar *path = file_path(file);
|
||||
gchar *tmp = file_write_path(file);
|
||||
|
||||
if (file->f_file) {
|
||||
fclose(file->f_file);
|
||||
if (file->f_mode == OPEN_WRITE || file->f_mode == OPEN_WRITE_BINARY)
|
||||
g_rename(tmp, path);
|
||||
}
|
||||
|
||||
file->f_file = NULL;
|
||||
file->f_mode = CLOSED;
|
||||
|
||||
g_free(path);
|
||||
g_free(tmp);
|
||||
}
|
||||
|
||||
int file_readf(struct file *file, const char *fmt, ...)
|
||||
gchar *file_readw(struct file *file)
|
||||
{
|
||||
va_list argp;
|
||||
int ret;
|
||||
|
||||
va_start(argp, fmt);
|
||||
ret = vfscanf(file->f_file, fmt, argp);
|
||||
va_end(argp);
|
||||
|
||||
return ret;
|
||||
gchar *s;
|
||||
return fscanf(file->f_file, "%ms%*c", &s) ? s : g_strdup("");
|
||||
}
|
||||
|
||||
gchar *file_readl(struct file *file)
|
||||
{
|
||||
gchar *res;
|
||||
gchar *s = NULL;
|
||||
size_t len = 0;
|
||||
return getline(&s, &len, file->f_file) ? g_strchomp(s) : g_strdup("");
|
||||
}
|
||||
|
||||
if (file_readf(file, "%m[^\n]\n", &res) == 0)
|
||||
return g_strdup("");
|
||||
|
||||
g_strstrip(res);
|
||||
return res;
|
||||
unsigned int file_readu(struct file *file)
|
||||
{
|
||||
unsigned int u;
|
||||
return fscanf(file->f_file, "%u%*c", &u) ? u : 0;
|
||||
}
|
||||
|
||||
int file_writef(struct file *file, const char *fmt, ...)
|
||||
|
@ -149,6 +218,62 @@ int file_writef(struct file *file, const char *fmt, ...)
|
|||
va_end(argp);
|
||||
|
||||
if (ret < 0)
|
||||
REPORT_ERROR(file->f_name);
|
||||
REPORT_ERRNO(file->f_name);
|
||||
return ret;
|
||||
}
|
||||
|
||||
gchar *file_read(struct file *file)
|
||||
{
|
||||
int fd = fileno(file->f_file);
|
||||
struct stat st;
|
||||
gchar *buf;
|
||||
|
||||
if (fstat(fd, &st) < 0)
|
||||
return NULL;
|
||||
|
||||
buf = g_malloc0(st.st_size + 1);
|
||||
if (fread(buf, st.st_size, 1, file->f_file) == 1)
|
||||
return buf;
|
||||
|
||||
g_free(buf);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int file_write(struct file *file, const void *data, size_t len)
|
||||
{
|
||||
if (fwrite(data, len, 1, file->f_file) == 1)
|
||||
return len;
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool file_import(struct file *file, const gchar *srcpath)
|
||||
{
|
||||
gchar *contents = NULL;
|
||||
gsize length = 0;
|
||||
|
||||
if (!file->f_file || !srcpath)
|
||||
return false;
|
||||
if (!g_file_get_contents(srcpath, &contents, &length, NULL))
|
||||
return false;
|
||||
|
||||
file_write(file, contents, length);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool file_remove(struct file *file)
|
||||
{
|
||||
gchar *path, *dir;
|
||||
int ret = -1;
|
||||
|
||||
if (!file->f_file) {
|
||||
path = file_path(file);
|
||||
ret = g_unlink(path);
|
||||
dir = g_path_get_dirname(path);
|
||||
if (string_length(file->f_subdir) > 0)
|
||||
g_rmdir(dir);
|
||||
g_free(path);
|
||||
g_free(dir);
|
||||
}
|
||||
|
||||
return ret == 0;
|
||||
}
|
||||
|
|
|
@ -1,69 +0,0 @@
|
|||
/*
|
||||
* Copyright 2013 (c) Anna Schumaker.
|
||||
*/
|
||||
|
||||
#include <core/containers/index.h>
|
||||
#include <core/filter.h>
|
||||
#include <core/string.h>
|
||||
|
||||
|
||||
static struct database filter_index;
|
||||
|
||||
void filter_init() { index_init(&filter_index, "", false); }
|
||||
void filter_deinit() { db_deinit(&filter_index); }
|
||||
|
||||
void filter_add(const gchar *text, unsigned int index)
|
||||
{
|
||||
const gchar *c = g_utf8_next_char(text);
|
||||
glong begin, end;
|
||||
gchar *substr;
|
||||
|
||||
for (begin = 0, end = 1; end <= g_utf8_strlen(text, -1); end++) {
|
||||
substr = g_utf8_substring(text, begin, end);
|
||||
index_insert(&filter_index, substr, index);
|
||||
|
||||
if (g_unichar_isspace(g_utf8_get_char(c))) {
|
||||
c = g_utf8_next_char(c);
|
||||
begin = ++end;
|
||||
}
|
||||
|
||||
c = g_utf8_next_char(c);
|
||||
g_free(substr);
|
||||
}
|
||||
}
|
||||
|
||||
void filter_search(const gchar *text, struct set *res)
|
||||
{
|
||||
gchar *lower = string_lowercase(text);
|
||||
struct index_entry *found;
|
||||
glong begin, end;
|
||||
gchar *c, *substr;
|
||||
|
||||
set_clear(res);
|
||||
c = lower;
|
||||
|
||||
for (begin = 0, end = 1; end <= g_utf8_strlen(lower, -1); end++) {
|
||||
c = g_utf8_next_char(c);
|
||||
if ((*c != '\0') && !g_unichar_isspace(g_utf8_get_char(c)))
|
||||
continue;
|
||||
|
||||
substr = g_utf8_substring(lower, begin, end);
|
||||
found = INDEX_ENTRY(db_get(&filter_index, substr));
|
||||
g_free(substr);
|
||||
|
||||
if (!found) {
|
||||
set_clear(res);
|
||||
break;
|
||||
}
|
||||
|
||||
if (begin == 0)
|
||||
set_copy(&found->ie_set, res);
|
||||
else
|
||||
set_inline_intersect(&found->ie_set, res);
|
||||
|
||||
c = g_utf8_next_char(c);
|
||||
begin = ++end;
|
||||
}
|
||||
|
||||
g_free(lower);
|
||||
}
|
97
core/idle.c
97
core/idle.c
|
@ -2,28 +2,86 @@
|
|||
* Copyright 2013 (c) Anna Schumaker.
|
||||
*/
|
||||
#include <core/idle.h>
|
||||
|
||||
#include <glib.h>
|
||||
|
||||
|
||||
struct idle_task {
|
||||
void (*idle_func)(void *);
|
||||
bool (*idle_func)(void *);
|
||||
void *idle_data;
|
||||
};
|
||||
|
||||
static GQueue idle_queue = G_QUEUE_INIT;
|
||||
static float queued = 0.0;
|
||||
static float serviced = 0.0;
|
||||
static GThreadPool *idle_pool = NULL;
|
||||
static GQueue idle_queue = G_QUEUE_INIT;
|
||||
static enum idle_sync_t idle_mode = IDLE_SYNC;
|
||||
static unsigned int queued = 0;
|
||||
static unsigned int serviced = 0;
|
||||
|
||||
|
||||
void idle_schedule(void (*func)(void *), void *data)
|
||||
void __idle_free_task(struct idle_task *task)
|
||||
{
|
||||
struct idle_task *task = g_malloc(sizeof(struct idle_task));
|
||||
g_free(task);
|
||||
g_atomic_int_inc(&serviced);
|
||||
}
|
||||
|
||||
bool __idle_run_task(struct idle_task *task)
|
||||
{
|
||||
bool finished = task->idle_func(task->idle_data);
|
||||
if (finished)
|
||||
__idle_free_task(task);
|
||||
return finished;
|
||||
}
|
||||
|
||||
void __idle_thread(gpointer task, gpointer data)
|
||||
{
|
||||
if (!__idle_run_task(task))
|
||||
g_thread_pool_push(idle_pool, task, NULL);
|
||||
}
|
||||
|
||||
|
||||
void idle_init(enum idle_sync_t sync)
|
||||
{
|
||||
idle_mode = sync;
|
||||
}
|
||||
|
||||
void idle_deinit()
|
||||
{
|
||||
struct idle_task *task;
|
||||
|
||||
while (!g_queue_is_empty(&idle_queue)) {
|
||||
task = g_queue_pop_head(&idle_queue);
|
||||
g_free(task);
|
||||
}
|
||||
|
||||
if (idle_pool) {
|
||||
g_thread_pool_free(idle_pool, true, true);
|
||||
idle_pool = NULL;
|
||||
}
|
||||
|
||||
queued = 0;
|
||||
serviced = 0;
|
||||
}
|
||||
|
||||
void idle_schedule(enum idle_sync_t sync, bool (*func)(void *), void *data)
|
||||
{
|
||||
struct idle_task *task;
|
||||
|
||||
if (sync == IDLE_ASYNC && idle_mode == IDLE_SYNC)
|
||||
return;
|
||||
|
||||
task = g_malloc(sizeof(struct idle_task));
|
||||
task->idle_func = func;
|
||||
task->idle_data = data;
|
||||
|
||||
g_queue_push_tail(&idle_queue, task);
|
||||
queued++;
|
||||
if (sync == IDLE_SYNC)
|
||||
g_queue_push_tail(&idle_queue, task);
|
||||
else {
|
||||
if (!idle_pool)
|
||||
idle_pool = g_thread_pool_new(__idle_thread, NULL, 1,
|
||||
false, NULL);
|
||||
g_thread_pool_push(idle_pool, task, NULL);
|
||||
}
|
||||
|
||||
g_atomic_int_inc(&queued);
|
||||
}
|
||||
|
||||
bool idle_run_task()
|
||||
|
@ -32,21 +90,22 @@ bool idle_run_task()
|
|||
|
||||
if (!g_queue_is_empty(&idle_queue)) {
|
||||
task = g_queue_pop_head(&idle_queue);
|
||||
task->idle_func(task->idle_data);
|
||||
g_free(task);
|
||||
serviced++;
|
||||
if (!__idle_run_task(task))
|
||||
g_queue_push_tail(&idle_queue, task);
|
||||
}
|
||||
|
||||
if (g_queue_is_empty(&idle_queue)) {
|
||||
queued = 0.0;
|
||||
serviced = 0.0;
|
||||
}
|
||||
return !g_queue_is_empty(&idle_queue);
|
||||
if (g_atomic_int_get(&queued) != g_atomic_int_get(&serviced))
|
||||
return true;
|
||||
|
||||
queued = 0;
|
||||
serviced = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
float idle_progress()
|
||||
{
|
||||
if (g_queue_is_empty(&idle_queue))
|
||||
if (g_atomic_int_get(&serviced) == 0 &&
|
||||
g_atomic_int_get(&queued) == 0)
|
||||
return 1.0;
|
||||
return serviced / queued;
|
||||
return (float)g_atomic_int_get(&serviced) / (float)queued;
|
||||
}
|
||||
|
|
244
core/library.cpp
244
core/library.cpp
|
@ -1,244 +0,0 @@
|
|||
/**
|
||||
* Copyright 2013 (c) Anna Schumaker.
|
||||
*/
|
||||
extern "C" {
|
||||
#include <core/idle.h>
|
||||
}
|
||||
#include <core/library.h>
|
||||
#include <core/print.h>
|
||||
|
||||
#include <glib.h>
|
||||
|
||||
|
||||
class LibraryQueue : public queue {
|
||||
private:
|
||||
file f;
|
||||
|
||||
public:
|
||||
|
||||
LibraryQueue()
|
||||
{
|
||||
file_init(&f, "library.q", 0);
|
||||
}
|
||||
|
||||
void save()
|
||||
{
|
||||
GSList *cur = q_sort;
|
||||
int field;
|
||||
|
||||
if (!file_open(&f, OPEN_WRITE))
|
||||
return;
|
||||
|
||||
file_writef(&f, "%u %u", q_flags, g_slist_length(q_sort));
|
||||
while (cur) {
|
||||
field = GPOINTER_TO_INT(cur->data);
|
||||
file_writef(&f, " %u %d", abs(field) - 1, field > 0);
|
||||
cur = g_slist_next(cur);
|
||||
}
|
||||
file_writef(&f, "\n");
|
||||
file_close(&f);
|
||||
}
|
||||
|
||||
void load()
|
||||
{
|
||||
unsigned int field;
|
||||
int ascending;
|
||||
unsigned int n;
|
||||
|
||||
if (!file_open(&f, OPEN_READ))
|
||||
return;
|
||||
|
||||
file_readf(&f, "%u %u", &q_flags, &n);
|
||||
for (unsigned int i = 0; i < n; i++) {
|
||||
file_readf(&f, "%u %d", &field, &ascending);
|
||||
queue_sort(this, (compare_t)(field + 1), (i == 0) ? true : false);
|
||||
if (ascending == false)
|
||||
queue_sort(this, (compare_t)(field + 1), false);
|
||||
}
|
||||
file_close(&f);
|
||||
}
|
||||
};
|
||||
|
||||
static LibraryQueue library_q;
|
||||
|
||||
struct scan_info {
|
||||
struct library *library;
|
||||
std :: string path;
|
||||
};
|
||||
|
||||
static void scan_path(void *);
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* Scanning functions are here
|
||||
*/
|
||||
static void tag_track(struct library *library, const std::string &filepath)
|
||||
{
|
||||
struct track *track = track_add(library, filepath.c_str());
|
||||
if (track)
|
||||
queue_add(&library_q, track);
|
||||
}
|
||||
|
||||
static void process_path(struct library *library, const std :: string &dir,
|
||||
const std :: string &name)
|
||||
{
|
||||
struct scan_info *scan = new struct scan_info;
|
||||
scan->library = library;
|
||||
scan->path = dir + "/" + name;
|
||||
|
||||
if (g_file_test(scan->path.c_str(), G_FILE_TEST_IS_DIR) == true)
|
||||
idle_schedule (scan_path, scan);
|
||||
else {
|
||||
tag_track(library, scan->path);
|
||||
delete scan;
|
||||
}
|
||||
}
|
||||
|
||||
static void scan_path(void *data)
|
||||
{
|
||||
GDir *dir;
|
||||
const char *name;
|
||||
struct scan_info *scan = (struct scan_info *)data;
|
||||
|
||||
dir = g_dir_open(scan->path.c_str(), 0, NULL);
|
||||
if (dir == NULL)
|
||||
return;
|
||||
|
||||
name = g_dir_read_name(dir);
|
||||
while (name != NULL) {
|
||||
process_path(scan->library, scan->path, name);
|
||||
name = g_dir_read_name(dir);
|
||||
}
|
||||
|
||||
track_db_commit();
|
||||
g_dir_close(dir);
|
||||
delete scan;
|
||||
}
|
||||
|
||||
static void validate_library(void *data)
|
||||
{
|
||||
struct library *library = (struct library *)data;
|
||||
struct db_entry *dbe, *next;
|
||||
struct track *track;
|
||||
gchar *path;
|
||||
|
||||
db_for_each(dbe, next, track_db_get()) {
|
||||
track = TRACK(dbe);
|
||||
if (track->tr_library != library)
|
||||
continue;
|
||||
|
||||
path = track_path(track);
|
||||
if (g_file_test(path, G_FILE_TEST_EXISTS) == false) {
|
||||
queue_remove_all(&library_q, track);
|
||||
track_remove(track);
|
||||
}
|
||||
g_free(path);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* External API begins here
|
||||
*/
|
||||
|
||||
void collection :: init(struct queue_ops *ops)
|
||||
{
|
||||
struct db_entry *track, *next;
|
||||
|
||||
queue_init(&library_q, Q_ENABLED | Q_REPEAT | Q_ADD_FRONT, ops);
|
||||
|
||||
db_for_each(track, next, track_db_get()) {
|
||||
if (TRACK(track)->tr_library->li_enabled)
|
||||
queue_add(&library_q, TRACK(track));
|
||||
}
|
||||
queue_unset_flag(&library_q, Q_ADD_FRONT);
|
||||
|
||||
library_q.load();
|
||||
if (!library_q.q_sort) {
|
||||
queue_sort(&library_q, COMPARE_ARTIST, true);
|
||||
queue_sort(&library_q, COMPARE_YEAR, false);
|
||||
queue_sort(&library_q, COMPARE_TRACK, false);
|
||||
}
|
||||
|
||||
queue_set_flag(&library_q, Q_SAVE_SORT);
|
||||
queue_set_flag(&library_q, Q_SAVE_FLAGS);
|
||||
}
|
||||
|
||||
void collection :: save(struct queue *queue, enum queue_flags flag)
|
||||
{
|
||||
library_q.save();
|
||||
}
|
||||
|
||||
struct library *collection :: add(const std::string &dir)
|
||||
{
|
||||
struct library *library = NULL;
|
||||
|
||||
if (g_file_test(dir.c_str(), G_FILE_TEST_IS_DIR) == false)
|
||||
return library;
|
||||
|
||||
library = library_find(dir.c_str());
|
||||
if (library)
|
||||
update(library);
|
||||
return library;
|
||||
}
|
||||
|
||||
void collection :: remove(struct library *library)
|
||||
{
|
||||
if (library) {
|
||||
set_enabled(library, false);
|
||||
track_remove_all(library);
|
||||
library_remove(library);
|
||||
}
|
||||
}
|
||||
|
||||
void collection :: update(struct library *library)
|
||||
{
|
||||
struct scan_info *scan = new struct scan_info;
|
||||
scan->library = library;
|
||||
|
||||
if (library) {
|
||||
scan->path = library->li_path;
|
||||
idle_schedule(validate_library, library);
|
||||
idle_schedule(scan_path, scan);
|
||||
}
|
||||
}
|
||||
|
||||
void collection :: update_all()
|
||||
{
|
||||
struct db_entry *library, *next;
|
||||
|
||||
db_for_each(library, next, library_db_get())
|
||||
update(LIBRARY(library));
|
||||
}
|
||||
|
||||
void collection :: set_enabled(struct library *library, bool enabled)
|
||||
{
|
||||
struct db_entry *dbe, *next;
|
||||
struct track *track;
|
||||
|
||||
if (!library || (library->li_enabled == enabled))
|
||||
return;
|
||||
|
||||
library_set_enabled(library, enabled);
|
||||
queue_set_flag(&library_q, Q_ADD_FRONT);
|
||||
|
||||
db_for_each(dbe, next, track_db_get()) {
|
||||
track = TRACK(dbe);
|
||||
if (track->tr_library == library) {
|
||||
if (enabled)
|
||||
queue_add(&library_q, track);
|
||||
else
|
||||
queue_remove_all(&library_q, track);
|
||||
}
|
||||
}
|
||||
|
||||
queue_unset_flag(&library_q, Q_ADD_FRONT);
|
||||
queue_resort(&library_q);
|
||||
}
|
||||
|
||||
queue *collection :: get_queue()
|
||||
{
|
||||
return &library_q;
|
||||
}
|
|
@ -0,0 +1,236 @@
|
|||
/*
|
||||
* Copyright 2013 (c) Anna Schumaker.
|
||||
*/
|
||||
#include <core/playlist.h>
|
||||
#include <core/settings.h>
|
||||
#include <core/string.h>
|
||||
|
||||
static const gchar *SETTINGS_CUR_TYPE = "core.playlist.cur.type";
|
||||
static const gchar *SETTINGS_CUR_ID = "core.playlist.cur.id";
|
||||
static const gchar *SETTINGS_PREV_TYPE = "core.playlist.prev.type";
|
||||
static const gchar *SETTINGS_PREV_ID = "core.playlist.prev.id";
|
||||
|
||||
static struct playlist *current = NULL;
|
||||
static struct playlist *previous = NULL;
|
||||
|
||||
struct playlist_type *playlist_types[] = {
|
||||
[PL_SYSTEM] = &pl_system,
|
||||
[PL_ARTIST] = &pl_artist,
|
||||
[PL_LIBRARY] = &pl_library,
|
||||
[PL_USER] = &pl_user,
|
||||
};
|
||||
|
||||
|
||||
static struct playlist *__playlist_saved(const gchar *s_type, const gchar *s_id)
|
||||
{
|
||||
unsigned int type, id;
|
||||
|
||||
if (!settings_has(s_type) || !settings_has(s_id))
|
||||
return NULL;
|
||||
|
||||
type = settings_get(s_type);
|
||||
id = settings_get(s_id);
|
||||
return playlist_types[type]->pl_get(id);
|
||||
}
|
||||
|
||||
void playlist_init(struct playlist_callbacks *cb)
|
||||
{
|
||||
playlist_generic_set_callbacks(cb);
|
||||
pl_system_init();
|
||||
pl_artist_init();
|
||||
pl_user_init();
|
||||
pl_library_init();
|
||||
|
||||
current = __playlist_saved(SETTINGS_CUR_TYPE, SETTINGS_CUR_ID);
|
||||
previous = __playlist_saved(SETTINGS_PREV_TYPE, SETTINGS_PREV_ID);
|
||||
if (!current)
|
||||
current = playlist_lookup(PL_SYSTEM, "Collection");
|
||||
}
|
||||
|
||||
void playlist_deinit()
|
||||
{
|
||||
pl_system_deinit();
|
||||
pl_artist_deinit();
|
||||
pl_user_deinit();
|
||||
pl_library_deinit();
|
||||
}
|
||||
|
||||
void playlist_save()
|
||||
{
|
||||
unsigned int i;
|
||||
for (i = 0; i < PL_MAX_TYPE; i++)
|
||||
playlist_types[i]->pl_save();
|
||||
}
|
||||
|
||||
void playlist_played(struct track *track)
|
||||
{
|
||||
unsigned int i;
|
||||
|
||||
if (track && !TRACK_IS_EXTERNAL(track)) {
|
||||
for (i = 0; i < PL_MAX_TYPE; i++)
|
||||
playlist_types[i]->pl_played(track);
|
||||
}
|
||||
}
|
||||
|
||||
void playlist_selected(struct track *track)
|
||||
{
|
||||
unsigned int i;
|
||||
if (track && !TRACK_IS_EXTERNAL(track)) {
|
||||
for (i = 0; i < PL_MAX_TYPE; i++)
|
||||
playlist_types[i]->pl_selected(track);
|
||||
|
||||
if (playlist_size(current) == 0)
|
||||
playlist_select(previous);
|
||||
}
|
||||
}
|
||||
|
||||
struct playlist *playlist_new(enum playlist_type_t type, const gchar *name)
|
||||
{
|
||||
if (type < PL_MAX_TYPE && playlist_types[type]->pl_new)
|
||||
return playlist_types[type]->pl_new(name);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool playlist_delete(struct playlist *playlist)
|
||||
{
|
||||
enum playlist_type_t type;
|
||||
bool ret;
|
||||
|
||||
if (!playlist || !playlist->pl_ops->pl_delete)
|
||||
return false;
|
||||
|
||||
type = playlist->pl_type;
|
||||
ret = playlist->pl_ops->pl_delete(playlist);
|
||||
if (ret)
|
||||
playlist_types[type]->pl_save();
|
||||
return ret;
|
||||
}
|
||||
|
||||
struct playlist *playlist_lookup(enum playlist_type_t type, const gchar *name)
|
||||
{
|
||||
if (type >= PL_MAX_TYPE)
|
||||
return NULL;
|
||||
return playlist_types[type]->pl_lookup(name);
|
||||
}
|
||||
|
||||
struct playlist *playlist_get(enum playlist_type_t type, unsigned int id)
|
||||
{
|
||||
if (type >= PL_MAX_TYPE)
|
||||
return NULL;
|
||||
return playlist_types[type]->pl_get(id);
|
||||
}
|
||||
|
||||
struct playlist *playlist_current(void)
|
||||
{
|
||||
return current;
|
||||
}
|
||||
|
||||
bool playlist_select(struct playlist *playlist)
|
||||
{
|
||||
if (!playlist || (playlist == current))
|
||||
return false;
|
||||
if (!playlist->pl_ops->pl_can_select)
|
||||
return false;
|
||||
if (!playlist->pl_ops->pl_can_select(playlist))
|
||||
return false;
|
||||
|
||||
previous = current;
|
||||
current = playlist;
|
||||
|
||||
settings_set(SETTINGS_CUR_TYPE, current->pl_type);
|
||||
settings_set(SETTINGS_CUR_ID, current->pl_id);
|
||||
|
||||
if (previous) {
|
||||
settings_set(SETTINGS_PREV_TYPE, previous->pl_type);
|
||||
settings_set(SETTINGS_PREV_ID, previous->pl_id);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
struct track *playlist_next(void)
|
||||
{
|
||||
struct track *track = playlist_generic_next(current);
|
||||
if (track && current->pl_type < PL_MAX_TYPE)
|
||||
playlist_types[current->pl_type]->pl_save();
|
||||
return track;
|
||||
}
|
||||
|
||||
struct track *playlist_prev(void)
|
||||
{
|
||||
return playlist_generic_next(playlist_lookup(PL_SYSTEM, "History"));
|
||||
}
|
||||
|
||||
bool playlist_add(struct playlist *playlist, struct track *track)
|
||||
{
|
||||
bool ret;
|
||||
|
||||
if (!track || !playlist || !playlist->pl_ops->pl_add)
|
||||
return false;
|
||||
|
||||
ret = playlist->pl_ops->pl_add(playlist, track);
|
||||
if (ret && playlist->pl_type < PL_MAX_TYPE)
|
||||
playlist_types[playlist->pl_type]->pl_save();
|
||||
if (playlist == playlist_lookup(PL_SYSTEM, "Queued Tracks"))
|
||||
playlist_select(playlist);
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool playlist_remove(struct playlist *playlist, struct track *track)
|
||||
{
|
||||
bool ret;
|
||||
|
||||
if (!track || !playlist || !playlist->pl_ops->pl_remove)
|
||||
return false;
|
||||
|
||||
ret = playlist->pl_ops->pl_remove(playlist, track);
|
||||
if (ret && playlist->pl_type < PL_MAX_TYPE)
|
||||
playlist_types[playlist->pl_type]->pl_save();
|
||||
return ret;
|
||||
}
|
||||
|
||||
void playlist_set_random(struct playlist *playlist, bool enabled)
|
||||
{
|
||||
if (playlist && playlist->pl_ops->pl_set_random) {
|
||||
playlist->pl_ops->pl_set_random(playlist, enabled);
|
||||
if (playlist->pl_type < PL_MAX_TYPE)
|
||||
playlist_types[playlist->pl_type]->pl_save();
|
||||
}
|
||||
}
|
||||
|
||||
bool playlist_sort(struct playlist *playlist, enum compare_t sort)
|
||||
{
|
||||
if (!playlist || !playlist->pl_ops->pl_sort)
|
||||
return false;
|
||||
|
||||
playlist->pl_ops->pl_sort(playlist, sort);
|
||||
if (playlist->pl_type < PL_MAX_TYPE)
|
||||
playlist_types[playlist->pl_type]->pl_save();
|
||||
return g_slist_length(playlist->pl_sort) > 0;
|
||||
}
|
||||
|
||||
bool playlist_rearrange(struct playlist *playlist, unsigned int old_pos,
|
||||
unsigned int new_pos)
|
||||
{
|
||||
bool ret;
|
||||
|
||||
if (!playlist || !playlist->pl_ops->pl_rearrange)
|
||||
return false;
|
||||
|
||||
ret = playlist->pl_ops->pl_rearrange(playlist, old_pos, new_pos);
|
||||
if (ret && playlist->pl_type < PL_MAX_TYPE)
|
||||
playlist_types[playlist->pl_type]->pl_save();
|
||||
return ret;
|
||||
}
|
||||
|
||||
void playlist_set_search(struct playlist *playlist, const gchar *text)
|
||||
{
|
||||
gchar **tokens = NULL;
|
||||
|
||||
if (!playlist)
|
||||
return;
|
||||
if (playlist->pl_search)
|
||||
g_strfreev(playlist->pl_search);
|
||||
if (strlen(text) > 0)
|
||||
tokens = g_str_tokenize_and_fold(text, NULL, NULL);
|
||||
playlist->pl_search = tokens;
|
||||
}
|
|
@ -1,153 +0,0 @@
|
|||
/**
|
||||
* Copyright 2013 (c) Anna Schumaker.
|
||||
*/
|
||||
#include <core/library.h>
|
||||
#include <core/playlist.h>
|
||||
|
||||
|
||||
class PlaylistQueue : public queue {
|
||||
public:
|
||||
|
||||
void clear()
|
||||
{
|
||||
queue_clear(this);
|
||||
}
|
||||
|
||||
void fill(index_entry *ent)
|
||||
{
|
||||
struct set_iter it;
|
||||
|
||||
clear();
|
||||
queue_set_flag(this, Q_ADD_FRONT);
|
||||
|
||||
set_for_each(&ent->ie_set, &it)
|
||||
queue_add(this, track_get(it.it_val));
|
||||
|
||||
queue_unset_flag(this, Q_ADD_FRONT);
|
||||
queue_resort(this);
|
||||
}
|
||||
|
||||
unsigned int find_average_count()
|
||||
{
|
||||
struct db_entry *track, *next;
|
||||
unsigned int total = 0, count = 0;
|
||||
|
||||
db_for_each(track, next, track_db_get()) {
|
||||
total += TRACK(track)->tr_count;
|
||||
count++;
|
||||
}
|
||||
|
||||
if (count > 0)
|
||||
return total / count;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void dynamic_add(const std::string &name, struct track *track, unsigned int avg)
|
||||
{
|
||||
if ((name == "Unplayed") && (track->tr_count == 0))
|
||||
queue_add(this, track);
|
||||
if ((name == "Most Played") && (track->tr_count > avg))
|
||||
queue_add(this, track);
|
||||
if ((name == "Least Played") && (track->tr_count < avg))
|
||||
queue_add(this, track);
|
||||
}
|
||||
|
||||
void dynamic_fill(const std::string &name)
|
||||
{
|
||||
struct db_entry *track, *next;
|
||||
unsigned int avg = 0;
|
||||
|
||||
if ((name == "Most Played") || (name == "Least Played"))
|
||||
avg = find_average_count();
|
||||
|
||||
clear();
|
||||
queue_set_flag(this, Q_ADD_FRONT);
|
||||
|
||||
db_for_each(track, next, track_db_get())
|
||||
dynamic_add(name, TRACK(track), avg);
|
||||
|
||||
queue_unset_flag(this, Q_ADD_FRONT);
|
||||
queue_resort(this);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
static struct database playlist_db;
|
||||
static PlaylistQueue playlist_q;
|
||||
static std::string cur_plist;
|
||||
|
||||
|
||||
void playlist :: init(struct queue_ops *ops)
|
||||
{
|
||||
struct set_iter it;
|
||||
|
||||
queue_init(&playlist_q, Q_ENABLED | Q_REPEAT, ops);
|
||||
queue_sort(&playlist_q, COMPARE_ARTIST, true);
|
||||
queue_sort(&playlist_q, COMPARE_YEAR, false);
|
||||
queue_sort(&playlist_q, COMPARE_TRACK, false);
|
||||
queue_set_flag(&playlist_q, Q_NO_SORT);
|
||||
|
||||
index_init(&playlist_db, "playlist.db", true);
|
||||
db_load(&playlist_db);
|
||||
|
||||
index_entry *ent = get_tracks("Banned");
|
||||
if (!ent)
|
||||
return;
|
||||
|
||||
set_for_each(&ent->ie_set, &it)
|
||||
queue_remove_all(collection :: get_queue(),
|
||||
track_get(it.it_val));
|
||||
}
|
||||
|
||||
bool playlist :: has(struct track *track, const std::string &name)
|
||||
{
|
||||
if (playlist_db.db_size == 0)
|
||||
return false;
|
||||
return index_has(&playlist_db, name.c_str(), track->tr_dbe.dbe_index);
|
||||
}
|
||||
|
||||
void playlist :: add(struct track *track, const std::string &name)
|
||||
{
|
||||
if (!( (name == "Banned") || (name == "Favorites") ))
|
||||
return;
|
||||
|
||||
if (!has(track, name)) {
|
||||
index_insert(&playlist_db, name.c_str(), track->tr_dbe.dbe_index);
|
||||
if (cur_plist == name)
|
||||
queue_add(&playlist_q, track);
|
||||
if (name == "Banned")
|
||||
queue_remove_all(collection :: get_queue(), track);
|
||||
}
|
||||
}
|
||||
|
||||
void playlist :: del(struct track *track, const std::string &name)
|
||||
{
|
||||
index_remove(&playlist_db, name.c_str(), track->tr_dbe.dbe_index);
|
||||
if (cur_plist == name)
|
||||
queue_remove_all(&playlist_q, track);
|
||||
if (name == "Banned")
|
||||
queue_add(collection :: get_queue(), track);
|
||||
}
|
||||
|
||||
void playlist :: select(const std::string &name)
|
||||
{
|
||||
index_entry *ent = INDEX_ENTRY(db_get(&playlist_db, name.c_str()));
|
||||
|
||||
if (ent != NULL)
|
||||
playlist_q.fill(ent);
|
||||
else
|
||||
playlist_q.dynamic_fill(name);
|
||||
|
||||
cur_plist = name;
|
||||
}
|
||||
|
||||
index_entry *playlist :: get_tracks(const std::string &name)
|
||||
{
|
||||
return INDEX_ENTRY(db_get(&playlist_db, name.c_str()));
|
||||
}
|
||||
|
||||
queue *playlist :: get_queue()
|
||||
{
|
||||
return &playlist_q;
|
||||
}
|
|
@ -0,0 +1,157 @@
|
|||
/*
|
||||
* Copyright 2016 (c) Anna Schumaker.
|
||||
*/
|
||||
#include <core/idle.h>
|
||||
#include <core/playlists/artist.h>
|
||||
#include <core/string.h>
|
||||
|
||||
static struct file artist_file = FILE_INIT_DATA("", "playlist.artist", 0);
|
||||
|
||||
static struct playlist_ops pl_artist_ops = {
|
||||
.pl_can_select = playlist_generic_can_select,
|
||||
.pl_set_random = playlist_generic_set_random,
|
||||
.pl_sort = playlist_generic_sort,
|
||||
.pl_rearrange = playlist_generic_rearrange,
|
||||
};
|
||||
|
||||
|
||||
static struct playlist *__artist_pl_alloc(struct artist *artist)
|
||||
{
|
||||
return playlist_generic_alloc(artist->ar_name, PL_ARTIST,
|
||||
artist_index(artist), &pl_artist_ops,
|
||||
3, COMPARE_YEAR, COMPARE_ALBUM, COMPARE_TRACK);
|
||||
}
|
||||
|
||||
static bool __artist_pl_add(void *data)
|
||||
{
|
||||
struct playlist *playlist = (struct playlist *)data;
|
||||
struct artist *artist = artist_lookup(playlist->pl_name);
|
||||
struct db_entry *dbe, *next;
|
||||
|
||||
db_for_each(dbe, next, track_db_get()) {
|
||||
if (TRACK(dbe)->tr_album->al_artist == artist)
|
||||
playlist_generic_add_front(playlist, TRACK(dbe));
|
||||
}
|
||||
|
||||
playlist_generic_resort(playlist);
|
||||
return true;
|
||||
}
|
||||
|
||||
static struct playlist *__artist_pl_lookup(const gchar *name)
|
||||
{
|
||||
struct artist *artist = artist_lookup(name);
|
||||
return artist ? artist->ar_playlist : NULL;
|
||||
}
|
||||
|
||||
static bool __artist_pl_load(void *data)
|
||||
{
|
||||
struct playlist *playlist;
|
||||
unsigned int i, n;
|
||||
gchar *name;
|
||||
|
||||
if (!file_open(&artist_file, OPEN_READ))
|
||||
return true;
|
||||
|
||||
n = file_readu(&artist_file);
|
||||
for (i = 0; i < n; i++) {
|
||||
name = file_readl(&artist_file);
|
||||
playlist = __artist_pl_lookup(name);
|
||||
if (playlist)
|
||||
playlist_generic_load(playlist, &artist_file,
|
||||
PL_SAVE_METADATA);
|
||||
g_free(name);
|
||||
}
|
||||
|
||||
file_close(&artist_file);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
static void pl_artist_save(void)
|
||||
{
|
||||
struct db_entry *dbe, *next;
|
||||
struct playlist *playlist;
|
||||
|
||||
if (!file_open(&artist_file, OPEN_WRITE))
|
||||
return;
|
||||
|
||||
file_writef(&artist_file, "%u\n", artist_db_get()->db_size);
|
||||
db_for_each(dbe, next, artist_db_get()) {
|
||||
playlist = ARTIST(dbe)->ar_playlist;
|
||||
file_writef(&artist_file, "%s\n", playlist->pl_name);
|
||||
playlist_generic_save(playlist, &artist_file, PL_SAVE_METADATA);
|
||||
}
|
||||
|
||||
file_close(&artist_file);
|
||||
}
|
||||
|
||||
static struct playlist *pl_artist_lookup(const gchar *name)
|
||||
{
|
||||
return __artist_pl_lookup(name);
|
||||
}
|
||||
|
||||
static struct playlist *pl_artist_get(unsigned int id)
|
||||
{
|
||||
struct artist *artist = artist_get(id);
|
||||
return artist ? artist->ar_playlist : NULL;
|
||||
}
|
||||
|
||||
static void pl_artist_played(struct track *track)
|
||||
{
|
||||
struct artist *artist = track->tr_album->al_artist;
|
||||
playlist_generic_update(artist->ar_playlist, track);
|
||||
}
|
||||
|
||||
|
||||
struct playlist_type pl_artist = {
|
||||
.pl_save = pl_artist_save,
|
||||
.pl_lookup = pl_artist_lookup,
|
||||
.pl_get = pl_artist_get,
|
||||
.pl_played = pl_artist_played,
|
||||
.pl_selected = pl_artist_played,
|
||||
};
|
||||
|
||||
|
||||
void pl_artist_init(void)
|
||||
{
|
||||
struct db_entry *dbe, *next;
|
||||
struct playlist *playlist;
|
||||
|
||||
db_for_each(dbe, next, artist_db_get()) {
|
||||
playlist = __artist_pl_alloc(ARTIST(dbe));
|
||||
ARTIST(dbe)->ar_playlist = playlist;
|
||||
|
||||
idle_schedule(IDLE_SYNC, __artist_pl_add, playlist);
|
||||
}
|
||||
|
||||
idle_schedule(IDLE_SYNC, __artist_pl_load, NULL);
|
||||
}
|
||||
|
||||
void pl_artist_deinit()
|
||||
{
|
||||
struct db_entry *dbe, *next;
|
||||
|
||||
db_for_each(dbe, next, artist_db_get()) {
|
||||
playlist_generic_free(ARTIST(dbe)->ar_playlist);
|
||||
ARTIST(dbe)->ar_playlist = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
void pl_artist_new_track(struct track *track)
|
||||
{
|
||||
struct artist *artist = track->tr_album->al_artist;
|
||||
struct playlist *playlist = (struct playlist *)artist->ar_playlist;
|
||||
|
||||
if (!playlist) {
|
||||
playlist = __artist_pl_alloc(artist);
|
||||
artist->ar_playlist = playlist;
|
||||
}
|
||||
|
||||
playlist_generic_add(playlist, track);
|
||||
}
|
||||
|
||||
void pl_artist_delete_track(struct track *track)
|
||||
{
|
||||
struct artist *artist = track->tr_album->al_artist;
|
||||
playlist_generic_remove(artist->ar_playlist, track);
|
||||
}
|
|
@ -0,0 +1,313 @@
|
|||
/*
|
||||
* Copyright 2016 (c) Anna Schumaker.
|
||||
*/
|
||||
#include <core/idle.h>
|
||||
#include <core/playlists/generic.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
static struct playlist_callbacks *callbacks = NULL;
|
||||
|
||||
|
||||
static int __playlist_generic_find_sort(gconstpointer a, gconstpointer b)
|
||||
{
|
||||
return abs(GPOINTER_TO_INT(a)) - abs(GPOINTER_TO_INT(b));
|
||||
}
|
||||
|
||||
static int __playlist_generic_less_than(gconstpointer a, gconstpointer b,
|
||||
gpointer data)
|
||||
{
|
||||
struct track *lhs = (struct track *)a;
|
||||
struct track *rhs = (struct track *)b;
|
||||
GSList *cur = ((struct playlist *)data)->pl_sort;
|
||||
int res, field;
|
||||
|
||||
while (cur) {
|
||||
field = GPOINTER_TO_INT(cur->data);
|
||||
res = track_compare(lhs, rhs, abs(field));
|
||||
if (res != 0)
|
||||
break;
|
||||
cur = g_slist_next(cur);
|
||||
};
|
||||
|
||||
return (field > 0) ? res : -res;
|
||||
}
|
||||
|
||||
|
||||
void playlist_generic_set_callbacks(struct playlist_callbacks *cb)
|
||||
{
|
||||
callbacks = cb;
|
||||
}
|
||||
|
||||
static void __playlist_generic_vinit(struct playlist *playlist,
|
||||
unsigned int nargs, va_list argp)
|
||||
{
|
||||
unsigned int i;
|
||||
|
||||
if (!playlist)
|
||||
return;
|
||||
|
||||
g_queue_init(&playlist->pl_tracks);
|
||||
playlist->pl_length = 0;
|
||||
playlist->pl_random = false;
|
||||
playlist->pl_current = NULL;
|
||||
playlist->pl_sort = NULL;
|
||||
playlist->pl_search = NULL;
|
||||
|
||||
for (i = 0; i < nargs; i++)
|
||||
playlist_generic_sort(playlist, va_arg(argp, unsigned int));
|
||||
}
|
||||
|
||||
void playlist_generic_init(struct playlist *playlist, unsigned int nargs, ...)
|
||||
{
|
||||
va_list argp;
|
||||
|
||||
va_start(argp, nargs);
|
||||
__playlist_generic_vinit(playlist, nargs, argp);
|
||||
va_end(argp);
|
||||
}
|
||||
|
||||
void playlist_generic_deinit(struct playlist *playlist)
|
||||
{
|
||||
if (playlist) {
|
||||
playlist_generic_clear(playlist);
|
||||
playlist_clear_sort(playlist);
|
||||
g_strfreev(playlist->pl_search);
|
||||
playlist->pl_search = NULL;
|
||||
playlist->pl_length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
struct playlist *playlist_generic_alloc(gchar *name, enum playlist_type_t type,
|
||||
unsigned int id, struct playlist_ops *ops,
|
||||
unsigned int nargs, ...)
|
||||
{
|
||||
struct playlist *playlist = g_malloc(sizeof(struct playlist));
|
||||
va_list argp;
|
||||
|
||||
playlist->pl_name = name;
|
||||
playlist->pl_type = type;
|
||||
playlist->pl_id = id;
|
||||
playlist->pl_ops = ops;
|
||||
|
||||
va_start(argp, nargs);
|
||||
__playlist_generic_vinit(playlist, nargs, argp);
|
||||
if (callbacks)
|
||||
callbacks->pl_cb_alloc(playlist);
|
||||
va_end(argp);
|
||||
|
||||
return playlist;
|
||||
}
|
||||
|
||||
void playlist_generic_free(struct playlist *playlist)
|
||||
{
|
||||
if (playlist) {
|
||||
playlist_generic_deinit(playlist);
|
||||
g_free(playlist);
|
||||
}
|
||||
}
|
||||
|
||||
void playlist_generic_save(struct playlist *playlist, struct file *file,
|
||||
unsigned int flags)
|
||||
{
|
||||
playlist_iter it;
|
||||
GSList *sort;
|
||||
int field;
|
||||
|
||||
if (!playlist)
|
||||
return;
|
||||
|
||||
if (flags & PL_SAVE_ITER)
|
||||
file_writef(file, "%u ", playlist_current_index(playlist));
|
||||
|
||||
if (flags & PL_SAVE_FLAGS) {
|
||||
sort = playlist->pl_sort;
|
||||
file_writef(file, "%u ", playlist->pl_random ? PL_RANDOM : 0);
|
||||
file_writef(file, "%u", g_slist_length(sort));
|
||||
while (sort) {
|
||||
field = GPOINTER_TO_INT(sort->data);
|
||||
file_writef(file, " %u %d", abs(field) - 1, field > 0);
|
||||
sort = g_slist_next(sort);
|
||||
}
|
||||
file_writef(file, "\n");
|
||||
}
|
||||
|
||||
if (flags & PL_SAVE_TRACKS) {
|
||||
file_writef(file, "%u", playlist_size(playlist));
|
||||
playlist_for_each(playlist, it)
|
||||
file_writef(file, " %u",
|
||||
track_index(playlist_iter_track(it)));
|
||||
file_writef(file, "\n");
|
||||
}
|
||||
}
|
||||
|
||||
void playlist_generic_load(struct playlist *playlist, struct file *file,
|
||||
unsigned int flags)
|
||||
{
|
||||
unsigned int f, n, i, t, it = 0;
|
||||
int field, ascending;
|
||||
|
||||
if (!playlist)
|
||||
return;
|
||||
|
||||
if (flags & PL_SAVE_ITER)
|
||||
it = file_readu(file);
|
||||
|
||||
if (flags & PL_SAVE_FLAGS) {
|
||||
f = file_readu(file);
|
||||
n = file_readu(file);
|
||||
playlist_clear_sort(playlist);
|
||||
for (i = 0; i < n; i++) {
|
||||
field = file_readu(file) + 1;
|
||||
ascending = file_readd(file);
|
||||
if (!ascending)
|
||||
field = -field;
|
||||
playlist->pl_sort = g_slist_append(playlist->pl_sort,
|
||||
GINT_TO_POINTER(field));
|
||||
}
|
||||
playlist_generic_resort(playlist);
|
||||
}
|
||||
|
||||
if (flags & PL_SAVE_TRACKS) {
|
||||
n = file_readu(file);
|
||||
for (i = 0; i < n; i++) {
|
||||
t = file_readu(file);
|
||||
playlist_generic_add(playlist, track_get(t));
|
||||
}
|
||||
}
|
||||
|
||||
playlist_generic_set_random(playlist, f == PL_RANDOM);
|
||||
playlist_current_set(playlist, it);
|
||||
}
|
||||
|
||||
bool playlist_generic_can_select(struct playlist *playlist)
|
||||
{
|
||||
return playlist_size(playlist) > 0;
|
||||
}
|
||||
|
||||
void playlist_generic_clear(struct playlist *playlist)
|
||||
{
|
||||
unsigned int n;
|
||||
|
||||
if (!playlist)
|
||||
return;
|
||||
|
||||
n = playlist_size(playlist);
|
||||
g_queue_clear(&playlist->pl_tracks);
|
||||
playlist->pl_length = 0;
|
||||
playlist->pl_current = NULL;
|
||||
if (callbacks)
|
||||
callbacks->pl_cb_removed(playlist, NULL, n);
|
||||
}
|
||||
|
||||
bool playlist_generic_add(struct playlist *playlist, struct track *track)
|
||||
{
|
||||
if (!playlist || !track || playlist_has(playlist, track))
|
||||
return false;
|
||||
|
||||
playlist->pl_length += track->tr_length;
|
||||
if (playlist->pl_sort) {
|
||||
g_queue_insert_sorted(&playlist->pl_tracks, track,
|
||||
__playlist_generic_less_than, playlist);
|
||||
} else
|
||||
g_queue_push_tail(&playlist->pl_tracks, track);
|
||||
|
||||
if (callbacks)
|
||||
callbacks->pl_cb_added(playlist, track);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool playlist_generic_add_front(struct playlist *playlist, struct track *track)
|
||||
{
|
||||
if (!playlist || !track)
|
||||
return false;
|
||||
|
||||
playlist->pl_length += track->tr_length;
|
||||
g_queue_push_head(&playlist->pl_tracks, track);
|
||||
if (callbacks)
|
||||
callbacks->pl_cb_added(playlist, track);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool playlist_generic_remove(struct playlist *playlist, struct track *track)
|
||||
{
|
||||
unsigned int count;
|
||||
|
||||
if (!playlist || !track)
|
||||
return false;
|
||||
|
||||
while (playlist_current_track(playlist) == track)
|
||||
playlist_current_previous(playlist);
|
||||
|
||||
count = g_queue_remove_all(&playlist->pl_tracks, track);
|
||||
playlist->pl_length -= (count * track->tr_length);
|
||||
|
||||
if (callbacks)
|
||||
callbacks->pl_cb_removed(playlist, track, count);
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
void playlist_generic_update(struct playlist *playlist, struct track *track)
|
||||
{
|
||||
if (playlist && callbacks)
|
||||
callbacks->pl_cb_updated(playlist, track);
|
||||
}
|
||||
|
||||
void playlist_generic_set_random(struct playlist *playlist, bool enabled)
|
||||
{
|
||||
playlist->pl_random = enabled;
|
||||
}
|
||||
|
||||
void playlist_generic_sort(struct playlist *playlist, enum compare_t field)
|
||||
{
|
||||
gpointer sort = GINT_TO_POINTER(field);
|
||||
GSList *found = g_slist_find_custom(playlist->pl_sort, sort,
|
||||
__playlist_generic_find_sort);
|
||||
|
||||
if (found)
|
||||
found->data = GINT_TO_POINTER(-field);
|
||||
else
|
||||
playlist->pl_sort = g_slist_append(playlist->pl_sort, sort);
|
||||
|
||||
playlist_generic_resort(playlist);
|
||||
}
|
||||
|
||||
void playlist_generic_resort(struct playlist *playlist)
|
||||
{
|
||||
if (!playlist || !playlist->pl_sort)
|
||||
return;
|
||||
|
||||
g_queue_sort(&playlist->pl_tracks, __playlist_generic_less_than, playlist);
|
||||
playlist_generic_update(playlist, NULL);
|
||||
}
|
||||
|
||||
bool playlist_generic_rearrange(struct playlist *playlist, unsigned int old_pos,
|
||||
unsigned int new_pos)
|
||||
{
|
||||
GList *nth;
|
||||
|
||||
if (old_pos == new_pos || old_pos >= playlist_size(playlist) ||
|
||||
new_pos > playlist_size(playlist))
|
||||
return false;
|
||||
|
||||
playlist_clear_sort(playlist);
|
||||
nth = g_queue_pop_nth_link(&playlist->pl_tracks, old_pos);
|
||||
g_queue_push_nth_link(&playlist->pl_tracks, new_pos, nth);
|
||||
playlist_generic_update(playlist, NULL);
|
||||
return true;
|
||||
}
|
||||
|
||||
struct track *playlist_generic_next(struct playlist *playlist)
|
||||
{
|
||||
unsigned int pos, size = playlist_size(playlist);
|
||||
|
||||
if (size == 0)
|
||||
return NULL;
|
||||
else if (playlist->pl_random) {
|
||||
pos = playlist_current_index(playlist);
|
||||
pos += g_random_int_range(1, size);
|
||||
playlist_current_set(playlist, pos % size);
|
||||
} else if (!playlist_current_next(playlist))
|
||||
playlist_current_set(playlist, 0);
|
||||
|
||||
return playlist_current_track(playlist);
|
||||
}
|
|
@ -0,0 +1,277 @@
|
|||
/*
|
||||
* Copyright 2016 (c) Anna Schumaker.
|
||||
*/
|
||||
#include <core/idle.h>
|
||||
#include <core/playlists/artist.h>
|
||||
#include <core/playlists/library.h>
|
||||
#include <core/playlists/system.h>
|
||||
#include <core/playlists/user.h>
|
||||
#include <unistd.h>
|
||||
|
||||
struct scan_data {
|
||||
struct library *sd_library;
|
||||
gchar *sd_path;
|
||||
};
|
||||
|
||||
static bool __lib_pl_scan_dir(void *);
|
||||
static struct file lib_file = FILE_INIT_DATA("", "playlist.library", 0);
|
||||
|
||||
static struct playlist_ops pl_library_ops;
|
||||
|
||||
|
||||
static struct playlist *__lib_pl_alloc(struct library *library)
|
||||
{
|
||||
return playlist_generic_alloc(library->li_path, PL_LIBRARY,
|
||||
library_index(library), &pl_library_ops,
|
||||
4, COMPARE_ARTIST, COMPARE_YEAR,
|
||||
COMPARE_ALBUM, COMPARE_TRACK);
|
||||
}
|
||||
|
||||
static bool __lib_pl_add(void *data)
|
||||
{
|
||||
struct playlist *playlist = (struct playlist *)data;
|
||||
struct library *library = library_lookup(playlist->pl_name);
|
||||
struct db_entry *dbe, *next;
|
||||
|
||||
db_for_each(dbe, next, track_db_get()) {
|
||||
if (TRACK(dbe)->tr_library == library)
|
||||
playlist_generic_add_front(playlist, TRACK(dbe));
|
||||
}
|
||||
|
||||
playlist_generic_resort(playlist);
|
||||
return true;
|
||||
}
|
||||
|
||||
static struct playlist *__lib_pl_lookup(const gchar *name)
|
||||
{
|
||||
struct library *library = library_lookup(name);
|
||||
return library ? library->li_playlist : NULL;
|
||||
}
|
||||
|
||||
static bool __lib_pl_load(void *data)
|
||||
{
|
||||
struct playlist *playlist;
|
||||
unsigned int i, n;
|
||||
gchar *name;
|
||||
|
||||
if (!file_open(&lib_file, OPEN_READ))
|
||||
return true;
|
||||
|
||||
n = file_readu(&lib_file);
|
||||
for (i = 0; i < n; i++) {
|
||||
name = file_readl(&lib_file);
|
||||
playlist = __lib_pl_lookup(name);
|
||||
if (playlist)
|
||||
playlist_generic_load(playlist, &lib_file,
|
||||
PL_SAVE_METADATA);
|
||||
g_free(name);
|
||||
}
|
||||
|
||||
file_close(&lib_file);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void __lib_pl_scan_dir_idle(struct library *library, const gchar *path)
|
||||
{
|
||||
struct scan_data *scan = g_malloc(sizeof(struct scan_data));
|
||||
|
||||
scan->sd_library = library;
|
||||
scan->sd_path = g_strdup(path);
|
||||
|
||||
/* scan data is freed by __lib_pl_scan_dir() */
|
||||
idle_schedule(IDLE_SYNC, __lib_pl_scan_dir, scan);
|
||||
}
|
||||
|
||||
static void __lib_pl_read_path(struct scan_data *scan, const gchar *name)
|
||||
{
|
||||
gchar *path = g_build_filename(scan->sd_path, name, NULL);
|
||||
struct playlist *playlist = scan->sd_library->li_playlist;
|
||||
struct track *track;
|
||||
|
||||
if (g_file_test(path, G_FILE_TEST_IS_DIR))
|
||||
__lib_pl_scan_dir_idle(scan->sd_library, path);
|
||||
else {
|
||||
track = track_add(scan->sd_library, path);
|
||||
if (track) {
|
||||
playlist_generic_add(playlist, track);
|
||||
pl_system_new_track(track);
|
||||
pl_artist_new_track(track);
|
||||
}
|
||||
}
|
||||
|
||||
g_free(path);
|
||||
}
|
||||
|
||||
static bool __lib_pl_scan_dir(void *data)
|
||||
{
|
||||
struct scan_data *scan = (struct scan_data *)data;
|
||||
const gchar *name;
|
||||
GDir *dir;
|
||||
|
||||
dir = g_dir_open(scan->sd_path, 0, NULL);
|
||||
if (!dir)
|
||||
goto out;
|
||||
|
||||
name = g_dir_read_name(dir);
|
||||
while (name != NULL) {
|
||||
__lib_pl_read_path(scan, name);
|
||||
name = g_dir_read_name(dir);
|
||||
}
|
||||
|
||||
g_dir_close(dir);
|
||||
track_db_commit();
|
||||
|
||||
out:
|
||||
/* Allocated by __lib_pl_scan_dir_idle() */
|
||||
g_free(scan->sd_path);
|
||||
g_free(scan);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool __lib_pl_update(void *data)
|
||||
{
|
||||
struct playlist *playlist = (struct playlist *)data;
|
||||
struct library *library = library_lookup(playlist->pl_name);
|
||||
struct db_entry *dbe, *next;
|
||||
gchar *path;
|
||||
|
||||
db_for_each(dbe, next, track_db_get()) {
|
||||
if (TRACK(dbe)->tr_library != library)
|
||||
continue;
|
||||
|
||||
path = track_path(TRACK(dbe));
|
||||
if (g_access(path, F_OK) < 0) {
|
||||
pl_system_delete_track(TRACK(dbe));
|
||||
pl_artist_delete_track(TRACK(dbe));
|
||||
playlist_generic_remove(playlist, TRACK(dbe));
|
||||
track_remove(TRACK(dbe));
|
||||
}
|
||||
g_free(path);
|
||||
}
|
||||
|
||||
track_db_commit();
|
||||
__lib_pl_scan_dir_idle(library, library->li_path);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
static bool pl_library_delete(struct playlist *playlist)
|
||||
{
|
||||
struct library *library = library_lookup(playlist->pl_name);
|
||||
playlist_iter it;
|
||||
|
||||
if (!library)
|
||||
return false;
|
||||
|
||||
playlist_for_each(playlist, it) {
|
||||
pl_system_delete_track(playlist_iter_track(it));
|
||||
pl_artist_delete_track(playlist_iter_track(it));
|
||||
pl_user_delete_track(playlist_iter_track(it));
|
||||
}
|
||||
playlist_generic_free(playlist);
|
||||
|
||||
track_remove_all(library);
|
||||
library_remove(library);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
static struct playlist_ops pl_library_ops = {
|
||||
.pl_can_select = playlist_generic_can_select,
|
||||
.pl_delete = pl_library_delete,
|
||||
.pl_set_random = playlist_generic_set_random,
|
||||
.pl_sort = playlist_generic_sort,
|
||||
.pl_rearrange = playlist_generic_rearrange,
|
||||
};
|
||||
|
||||
|
||||
static void pl_library_save(void)
|
||||
{
|
||||
struct db_entry *dbe, *next;
|
||||
struct playlist *playlist;
|
||||
|
||||
if (!file_open(&lib_file, OPEN_WRITE))
|
||||
return;
|
||||
|
||||
file_writef(&lib_file, "%u\n", library_db_get()->db_size);
|
||||
db_for_each(dbe, next, library_db_get()) {
|
||||
playlist = LIBRARY(dbe)->li_playlist;
|
||||
file_writef(&lib_file, "%s\n", playlist->pl_name);
|
||||
playlist_generic_save(playlist, &lib_file, PL_SAVE_METADATA);
|
||||
}
|
||||
|
||||
file_close(&lib_file);
|
||||
}
|
||||
|
||||
static struct playlist *pl_library_lookup(const gchar *name)
|
||||
{
|
||||
return __lib_pl_lookup(name);
|
||||
}
|
||||
|
||||
static struct playlist *pl_library_get(unsigned int id)
|
||||
{
|
||||
struct library *library = LIBRARY(db_at(library_db_get(), id));
|
||||
return library ? library->li_playlist : NULL;
|
||||
}
|
||||
|
||||
static struct playlist *pl_library_new(const gchar *name)
|
||||
{
|
||||
struct library *library;
|
||||
|
||||
if (__lib_pl_lookup(name) || !g_file_test(name, G_FILE_TEST_IS_DIR))
|
||||
return NULL;
|
||||
|
||||
library = library_find(name);
|
||||
library->li_playlist = __lib_pl_alloc(library);
|
||||
|
||||
__lib_pl_scan_dir_idle(library, name);
|
||||
return library->li_playlist;
|
||||
}
|
||||
|
||||
static void pl_library_played(struct track *track)
|
||||
{
|
||||
struct library *library = track->tr_library;
|
||||
playlist_generic_update(library->li_playlist, track);
|
||||
}
|
||||
|
||||
|
||||
struct playlist_type pl_library = {
|
||||
.pl_save = pl_library_save,
|
||||
.pl_lookup = pl_library_lookup,
|
||||
.pl_get = pl_library_get,
|
||||
.pl_new = pl_library_new,
|
||||
.pl_played = pl_library_played,
|
||||
.pl_selected = pl_library_played,
|
||||
};
|
||||
|
||||
|
||||
void pl_library_init(void)
|
||||
{
|
||||
struct db_entry *dbe, *next;
|
||||
struct playlist *playlist;
|
||||
|
||||
db_for_each(dbe, next, library_db_get()) {
|
||||
playlist = __lib_pl_alloc(LIBRARY(dbe));
|
||||
LIBRARY(dbe)->li_playlist = playlist;
|
||||
|
||||
idle_schedule(IDLE_SYNC, __lib_pl_add, playlist);
|
||||
idle_schedule(IDLE_SYNC, __lib_pl_update, playlist);
|
||||
}
|
||||
|
||||
idle_schedule(IDLE_SYNC, __lib_pl_load, NULL);
|
||||
}
|
||||
|
||||
void pl_library_deinit()
|
||||
{
|
||||
struct db_entry *dbe, *next;
|
||||
|
||||
db_for_each(dbe, next, library_db_get()) {
|
||||
playlist_generic_free(LIBRARY(dbe)->li_playlist);
|
||||
LIBRARY(dbe)->li_playlist = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
void pl_library_update(struct playlist *playlist)
|
||||
{
|
||||
idle_schedule(IDLE_SYNC, __lib_pl_update, playlist);
|
||||
}
|
|
@ -0,0 +1,454 @@
|
|||
/*
|
||||
* Copyright 2016 (c) Anna Schumaker.
|
||||
*/
|
||||
#include <core/idle.h>
|
||||
#include <core/playlists/system.h>
|
||||
#include <core/string.h>
|
||||
|
||||
static struct playlist *pl_system_lookup(const gchar *);
|
||||
static struct playlist *pl_system_get(unsigned int);
|
||||
static void pl_system_save();
|
||||
|
||||
static struct file sys_file = FILE_INIT_DATA("", "playlist.db", 0);
|
||||
static struct file sys_deck_f = FILE_INIT_DATA("", "deck", 1);
|
||||
static struct file sys_collection_f = FILE_INIT_DATA("", "library.q", 0);
|
||||
static struct file sys_pl_file = FILE_INIT_DATA("", "playlist.system", 0);
|
||||
|
||||
|
||||
/*
|
||||
* Generic system playlist operations.
|
||||
*/
|
||||
static bool sys_pl_delete_clear(struct playlist *playlist)
|
||||
{
|
||||
playlist_generic_clear(playlist);
|
||||
pl_system_save();
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool sys_pl_update_check(struct playlist *playlist, struct track *track)
|
||||
{
|
||||
switch (playlist->pl_id) {
|
||||
case SYS_PL_UNPLAYED:
|
||||
return track->tr_count == 0;
|
||||
case SYS_PL_LEAST_PLAYED:
|
||||
return track->tr_count <= track_db_average_plays() &&
|
||||
track->tr_count > 0;
|
||||
case SYS_PL_MOST_PLAYED:
|
||||
return track->tr_count > track_db_average_plays();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool sys_pl_update_func(void *data)
|
||||
{
|
||||
struct playlist *playlist = (struct playlist *)data;
|
||||
struct db_entry *dbe, *next;
|
||||
|
||||
db_for_each(dbe, next, track_db_get()) {
|
||||
struct track *track = TRACK(dbe);
|
||||
|
||||
if (!sys_pl_update_check(playlist, track))
|
||||
playlist_generic_remove(playlist, track);
|
||||
else if (!playlist_has(pl_system_get(SYS_PL_HIDDEN), track) &&
|
||||
!playlist_has(playlist, track))
|
||||
playlist_generic_add_front(playlist, track);
|
||||
}
|
||||
|
||||
playlist_generic_resort(playlist);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void sys_pl_update(struct playlist *playlist)
|
||||
{
|
||||
switch (playlist->pl_id) {
|
||||
case SYS_PL_COLLECTION:
|
||||
case SYS_PL_UNPLAYED:
|
||||
case SYS_PL_LEAST_PLAYED:
|
||||
case SYS_PL_MOST_PLAYED:
|
||||
idle_schedule(IDLE_SYNC, sys_pl_update_func, playlist);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Favorite tracks playlist operations.
|
||||
*/
|
||||
static struct playlist_ops favorites_ops = {
|
||||
.pl_add = playlist_generic_add,
|
||||
.pl_can_select = playlist_generic_can_select,
|
||||
.pl_delete = sys_pl_delete_clear,
|
||||
.pl_remove = playlist_generic_remove,
|
||||
.pl_set_random = playlist_generic_set_random,
|
||||
.pl_sort = playlist_generic_sort,
|
||||
.pl_rearrange = playlist_generic_rearrange,
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
* Hidden tracks playlist operations.
|
||||
*/
|
||||
static bool sys_pl_hidden_add(struct playlist *playlist, struct track *track)
|
||||
{
|
||||
bool ret = playlist_generic_add(pl_system_get(SYS_PL_HIDDEN), track);
|
||||
playlist_generic_remove(pl_system_get(SYS_PL_COLLECTION), track);
|
||||
playlist_generic_remove(pl_system_get(SYS_PL_UNPLAYED), track);
|
||||
playlist_generic_remove(pl_system_get(SYS_PL_MOST_PLAYED), track);
|
||||
playlist_generic_remove(pl_system_get(SYS_PL_LEAST_PLAYED), track);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static bool sys_pl_hidden_remove(struct playlist *playlist, struct track *track)
|
||||
{
|
||||
bool ret = playlist_generic_remove(playlist, track);
|
||||
unsigned int average = track_db_average_plays();
|
||||
unsigned int add_id = SYS_PL_LEAST_PLAYED;
|
||||
|
||||
add_id = (track->tr_count == 0) ? SYS_PL_UNPLAYED : add_id;
|
||||
add_id = (track->tr_count > average) ? SYS_PL_MOST_PLAYED : add_id;
|
||||
|
||||
playlist_generic_add(pl_system_get(SYS_PL_COLLECTION), track);
|
||||
playlist_generic_add(pl_system_get(add_id), track);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static bool sys_pl_hidden_clear(struct playlist *playlist)
|
||||
{
|
||||
struct track *track;
|
||||
|
||||
while (playlist_size(playlist) > 0) {
|
||||
track = playlist_at(playlist, 0);
|
||||
sys_pl_hidden_remove(playlist, track);
|
||||
}
|
||||
|
||||
pl_system_save();
|
||||
return false;
|
||||
}
|
||||
|
||||
static struct playlist_ops hidden_ops = {
|
||||
.pl_add = sys_pl_hidden_add,
|
||||
.pl_can_select = playlist_generic_can_select,
|
||||
.pl_delete = sys_pl_hidden_clear,
|
||||
.pl_remove = sys_pl_hidden_remove,
|
||||
.pl_set_random = playlist_generic_set_random,
|
||||
.pl_sort = playlist_generic_sort,
|
||||
.pl_rearrange = playlist_generic_rearrange,
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
* Queued tracks playlist operations.
|
||||
*/
|
||||
static bool sys_pl_queued_load()
|
||||
{
|
||||
struct playlist *playlist = pl_system_get(SYS_PL_QUEUED);
|
||||
unsigned int num, i, flags = 0;
|
||||
|
||||
if (!file_open(&sys_deck_f, OPEN_READ))
|
||||
return true;
|
||||
|
||||
num = file_readu(&sys_deck_f);
|
||||
for (i = 0; i < num; i++) {
|
||||
flags = file_readu(&sys_deck_f);
|
||||
flags &= PL_RANDOM;
|
||||
if (i == 0)
|
||||
playlist_generic_set_random(playlist,
|
||||
flags == PL_RANDOM);
|
||||
playlist_generic_load(playlist, &sys_deck_f, PL_SAVE_TRACKS);
|
||||
}
|
||||
|
||||
file_close(&sys_deck_f);
|
||||
file_remove(&sys_deck_f);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool sys_pl_queued_delete(struct playlist *playlist)
|
||||
{
|
||||
playlist_generic_set_random(playlist, false);
|
||||
playlist_clear_sort(playlist);
|
||||
return sys_pl_delete_clear(playlist);
|
||||
}
|
||||
|
||||
static bool sys_pl_queued_remove(struct playlist *playlist, struct track *track)
|
||||
{
|
||||
bool ret = playlist_generic_remove(playlist, track);
|
||||
if (playlist_size(playlist) == 0)
|
||||
sys_pl_queued_delete(playlist);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static struct playlist_ops queued_ops = {
|
||||
.pl_add = playlist_generic_add,
|
||||
.pl_can_select = playlist_generic_can_select,
|
||||
.pl_delete = sys_pl_queued_delete,
|
||||
.pl_remove = sys_pl_queued_remove,
|
||||
.pl_set_random = playlist_generic_set_random,
|
||||
.pl_sort = playlist_generic_sort,
|
||||
.pl_rearrange = playlist_generic_rearrange,
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
* Collection playlist operations.
|
||||
*/
|
||||
static bool sys_pl_collection_load()
|
||||
{
|
||||
struct playlist *playlist = pl_system_get(SYS_PL_COLLECTION);
|
||||
|
||||
if (file_open(&sys_collection_f, OPEN_READ)) {
|
||||
playlist_generic_load(playlist, &sys_collection_f, PL_SAVE_FLAGS);
|
||||
file_close(&sys_collection_f);
|
||||
file_remove(&sys_collection_f);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static struct playlist_ops collection_ops = {
|
||||
.pl_can_select = playlist_generic_can_select,
|
||||
.pl_remove = sys_pl_hidden_add,
|
||||
.pl_set_random = playlist_generic_set_random,
|
||||
.pl_sort = playlist_generic_sort,
|
||||
.pl_rearrange = playlist_generic_rearrange,
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
* History playlist operations.
|
||||
*/
|
||||
static bool sys_pl_history_add(struct playlist *playlist, struct track *track)
|
||||
{
|
||||
playlist_generic_add_front(playlist, track);
|
||||
playlist_current_set(playlist, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
static struct playlist_ops history_ops = {
|
||||
.pl_add = sys_pl_history_add,
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
* Unplayed, Most Played, and Least Played tracks playlist operations.
|
||||
*/
|
||||
static struct playlist_ops dynamic_ops = {
|
||||
.pl_can_select = playlist_generic_can_select,
|
||||
.pl_set_random = playlist_generic_set_random,
|
||||
.pl_sort = playlist_generic_sort,
|
||||
.pl_rearrange = playlist_generic_rearrange,
|
||||
};
|
||||
|
||||
|
||||
#define SYS_PLAYLIST(id, name, ops) \
|
||||
[id] = DEFINE_PLAYLIST(PL_SYSTEM, name, id, ops)
|
||||
|
||||
static struct playlist sys_playlists[SYS_PL_NUM_PLAYLISTS] = {
|
||||
SYS_PLAYLIST(SYS_PL_FAVORITES, "Favorites", &favorites_ops),
|
||||
SYS_PLAYLIST(SYS_PL_HIDDEN, "Hidden", &hidden_ops),
|
||||
SYS_PLAYLIST(SYS_PL_QUEUED, "Queued Tracks", &queued_ops),
|
||||
SYS_PLAYLIST(SYS_PL_COLLECTION, "Collection", &collection_ops),
|
||||
SYS_PLAYLIST(SYS_PL_HISTORY, "History", &history_ops),
|
||||
SYS_PLAYLIST(SYS_PL_UNPLAYED, "Unplayed", &dynamic_ops),
|
||||
SYS_PLAYLIST(SYS_PL_MOST_PLAYED, "Most Played", &dynamic_ops),
|
||||
SYS_PLAYLIST(SYS_PL_LEAST_PLAYED, "Least Played", &dynamic_ops),
|
||||
};
|
||||
|
||||
static bool __sys_pl_update_save()
|
||||
{
|
||||
pl_system_save();
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool __sys_pl_load()
|
||||
{
|
||||
struct playlist *playlist;
|
||||
unsigned int i, n;
|
||||
gchar *name;
|
||||
|
||||
if (!file_open(&sys_file, OPEN_READ))
|
||||
return true;
|
||||
|
||||
n = file_readu(&sys_file);
|
||||
for (i = 0; i < n; i++) {
|
||||
file_readu(&sys_file);
|
||||
name = file_readl(&sys_file);
|
||||
if (string_match(name, "Banned")) {
|
||||
g_free(name);
|
||||
name = g_strdup("Hidden");
|
||||
}
|
||||
playlist = pl_system_lookup(name);
|
||||
g_free(name);
|
||||
|
||||
if (playlist)
|
||||
playlist_generic_load(playlist, &sys_file, PL_SAVE_TRACKS);
|
||||
}
|
||||
|
||||
file_close(&sys_file);
|
||||
file_remove(&sys_file);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool __sys_pl_load_new()
|
||||
{
|
||||
struct playlist *playlist;
|
||||
unsigned int i, n, load;
|
||||
gchar *name;
|
||||
|
||||
if (!file_open(&sys_pl_file, OPEN_READ)) {
|
||||
__sys_pl_load();
|
||||
sys_pl_collection_load();
|
||||
sys_pl_queued_load();
|
||||
__sys_pl_update_save();
|
||||
return true;
|
||||
}
|
||||
|
||||
n = file_readu(&sys_pl_file);
|
||||
for (i = 0; i < n; i++) {
|
||||
load = PL_SAVE_METADATA;
|
||||
name = file_readl(&sys_pl_file);
|
||||
|
||||
if (string_match(name, "Banned")) {
|
||||
g_free(name);
|
||||
name = g_strdup("Hidden");
|
||||
}
|
||||
|
||||
playlist = pl_system_lookup(name);
|
||||
g_free(name);
|
||||
|
||||
switch (i) {
|
||||
case SYS_PL_FAVORITES:
|
||||
case SYS_PL_HIDDEN:
|
||||
case SYS_PL_QUEUED:
|
||||
load = PL_SAVE_ALL;
|
||||
}
|
||||
|
||||
playlist_generic_load(playlist, &sys_pl_file, load);
|
||||
}
|
||||
|
||||
file_close(&sys_pl_file);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
static void pl_system_save(void)
|
||||
{
|
||||
struct playlist *playlist;
|
||||
unsigned int i, save;
|
||||
|
||||
if (!file_open(&sys_pl_file, OPEN_WRITE))
|
||||
return;
|
||||
|
||||
file_writef(&sys_pl_file, "%u\n", SYS_PL_NUM_PLAYLISTS);
|
||||
for (i = 0; i < SYS_PL_NUM_PLAYLISTS; i++) {
|
||||
save = PL_SAVE_METADATA;
|
||||
playlist = pl_system_get(i);
|
||||
|
||||
switch (i) {
|
||||
case SYS_PL_FAVORITES:
|
||||
case SYS_PL_HIDDEN:
|
||||
case SYS_PL_QUEUED:
|
||||
save = PL_SAVE_ALL;
|
||||
}
|
||||
|
||||
file_writef(&sys_pl_file, "%s\n", playlist->pl_name);
|
||||
playlist_generic_save(playlist, &sys_pl_file, save);
|
||||
|
||||
}
|
||||
|
||||
file_close(&sys_pl_file);
|
||||
}
|
||||
|
||||
static struct playlist *pl_system_lookup(const gchar *name)
|
||||
{
|
||||
unsigned int i;
|
||||
|
||||
for (i = 0; i < SYS_PL_NUM_PLAYLISTS; i++) {
|
||||
if (string_match(name, pl_system_get(i)->pl_name))
|
||||
return pl_system_get(i);
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static struct playlist *pl_system_get(unsigned int id)
|
||||
{
|
||||
return (id < SYS_PL_NUM_PLAYLISTS) ? &sys_playlists[id] : NULL;
|
||||
}
|
||||
|
||||
static void pl_system_played(struct track *track)
|
||||
{
|
||||
unsigned int i;
|
||||
for (i = 0; i < SYS_PL_NUM_PLAYLISTS; i++)
|
||||
playlist_generic_update(pl_system_get(i), track);
|
||||
|
||||
sys_pl_update(pl_system_lookup("Unplayed"));
|
||||
sys_pl_update(pl_system_lookup("Most Played"));
|
||||
sys_pl_update(pl_system_lookup("Least Played"));
|
||||
}
|
||||
|
||||
static void pl_system_selected(struct track *track)
|
||||
{
|
||||
unsigned int i;
|
||||
|
||||
sys_pl_queued_remove(pl_system_get(SYS_PL_QUEUED), track);
|
||||
for (i = 0; i < SYS_PL_NUM_PLAYLISTS; i++)
|
||||
playlist_generic_update(pl_system_get(i), track);
|
||||
}
|
||||
|
||||
|
||||
struct playlist_type pl_system = {
|
||||
.pl_save = pl_system_save,
|
||||
.pl_lookup = pl_system_lookup,
|
||||
.pl_get = pl_system_get,
|
||||
.pl_played = pl_system_played,
|
||||
.pl_selected = pl_system_selected,
|
||||
};
|
||||
|
||||
|
||||
void pl_system_init(void)
|
||||
{
|
||||
struct playlist *playlist;
|
||||
unsigned int i;
|
||||
|
||||
idle_schedule(IDLE_SYNC, __sys_pl_load_new, NULL);
|
||||
|
||||
for (i = 0; i < SYS_PL_NUM_PLAYLISTS; i++) {
|
||||
playlist = pl_system_get(i);
|
||||
|
||||
switch (i) {
|
||||
case SYS_PL_QUEUED:
|
||||
case SYS_PL_HISTORY:
|
||||
playlist_generic_init(playlist, 0);
|
||||
break;
|
||||
case SYS_PL_COLLECTION:
|
||||
case SYS_PL_UNPLAYED:
|
||||
case SYS_PL_MOST_PLAYED:
|
||||
case SYS_PL_LEAST_PLAYED:
|
||||
sys_pl_update(playlist);
|
||||
case SYS_PL_FAVORITES:
|
||||
case SYS_PL_HIDDEN:
|
||||
playlist_generic_init(playlist, 4, COMPARE_ARTIST,
|
||||
COMPARE_YEAR, COMPARE_ALBUM, COMPARE_TRACK);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void pl_system_deinit()
|
||||
{
|
||||
for (unsigned int i = 0; i < SYS_PL_NUM_PLAYLISTS; i++)
|
||||
playlist_generic_deinit(pl_system_get(i));
|
||||
}
|
||||
|
||||
void pl_system_new_track(struct track *track)
|
||||
{
|
||||
playlist_generic_add(pl_system_lookup("Collection"), track);
|
||||
playlist_generic_add(pl_system_lookup("Unplayed"), track);
|
||||
}
|
||||
|
||||
void pl_system_delete_track(struct track *track)
|
||||
{
|
||||
for (unsigned int i = 0; i < SYS_PL_NUM_PLAYLISTS; i++)
|
||||
playlist_generic_remove(pl_system_get(i), track);
|
||||
}
|
|
@ -0,0 +1,178 @@
|
|||
/*
|
||||
* Copyright 2016 (c) Anna Schumaker.
|
||||
*/
|
||||
#include <core/playlists/user.h>
|
||||
|
||||
static struct database user_db;
|
||||
static struct playlist_ops user_ops;
|
||||
|
||||
static struct user_playlist *__user_db_alloc(gchar *name, unsigned int index)
|
||||
{
|
||||
struct user_playlist *playlist = g_malloc(sizeof(struct user_playlist));
|
||||
|
||||
dbe_init(&playlist->pl_dbe, playlist);
|
||||
playlist->pl_playlist.pl_name = name;
|
||||
playlist->pl_playlist.pl_type = PL_USER;
|
||||
playlist->pl_playlist.pl_id = index;
|
||||
playlist->pl_playlist.pl_ops = &user_ops;
|
||||
playlist_generic_init(&playlist->pl_playlist, 0);
|
||||
|
||||
return playlist;
|
||||
}
|
||||
|
||||
static struct db_entry *user_db_alloc(const gchar *name, unsigned int index)
|
||||
{
|
||||
return &__user_db_alloc(g_strdup(name), index)->pl_dbe;
|
||||
}
|
||||
|
||||
static void user_db_free(struct db_entry *dbe)
|
||||
{
|
||||
playlist_generic_deinit(&USER_PLAYLIST(dbe)->pl_playlist);
|
||||
g_free(USER_PLAYLIST(dbe)->pl_playlist.pl_name);
|
||||
g_free(USER_PLAYLIST(dbe));
|
||||
}
|
||||
|
||||
static gchar *user_db_key(struct db_entry *dbe)
|
||||
{
|
||||
return g_strdup(USER_PLAYLIST(dbe)->pl_playlist.pl_name);
|
||||
}
|
||||
|
||||
static struct db_entry *user_db_read(struct file *file, unsigned int index)
|
||||
{
|
||||
gchar *name = file_readl(file);
|
||||
struct user_playlist *playlist = __user_db_alloc(name, index);
|
||||
|
||||
playlist_generic_load(&playlist->pl_playlist, file, PL_SAVE_ALL);
|
||||
return &playlist->pl_dbe;
|
||||
}
|
||||
|
||||
static void user_db_write(struct file *file, struct db_entry *dbe)
|
||||
{
|
||||
struct playlist *playlist = &USER_PLAYLIST(dbe)->pl_playlist;
|
||||
|
||||
file_writef(file, "%s\n", playlist->pl_name);
|
||||
playlist_generic_save(playlist, file, PL_SAVE_ALL);
|
||||
}
|
||||
|
||||
|
||||
static const struct db_ops user_db_ops = {
|
||||
.dbe_alloc = user_db_alloc,
|
||||
.dbe_free = user_db_free,
|
||||
.dbe_key = user_db_key,
|
||||
.dbe_read = user_db_read,
|
||||
.dbe_write = user_db_write,
|
||||
};
|
||||
|
||||
|
||||
static bool pl_user_delete(struct playlist *playlist)
|
||||
{
|
||||
struct db_entry *dbe = db_get(&user_db, playlist->pl_name);
|
||||
if (dbe) {
|
||||
db_remove(&user_db, dbe);
|
||||
db_defrag(&user_db);
|
||||
}
|
||||
return dbe != NULL;
|
||||
}
|
||||
|
||||
|
||||
static struct playlist_ops user_ops = {
|
||||
.pl_add = playlist_generic_add,
|
||||
.pl_can_select = playlist_generic_can_select,
|
||||
.pl_delete = pl_user_delete,
|
||||
.pl_remove = playlist_generic_remove,
|
||||
.pl_set_random = playlist_generic_set_random,
|
||||
.pl_sort = playlist_generic_sort,
|
||||
.pl_rearrange = playlist_generic_rearrange,
|
||||
};
|
||||
|
||||
|
||||
static void pl_user_save(void)
|
||||
{
|
||||
db_save(&user_db);
|
||||
}
|
||||
|
||||
static struct playlist *pl_user_lookup(const gchar *name)
|
||||
{
|
||||
struct db_entry *dbe = db_get(&user_db, name);
|
||||
return dbe ? &USER_PLAYLIST(dbe)->pl_playlist : NULL;
|
||||
}
|
||||
|
||||
static struct playlist *pl_user_get(unsigned int id)
|
||||
{
|
||||
struct db_entry *dbe = db_at(&user_db, id);
|
||||
return dbe ? &USER_PLAYLIST(dbe)->pl_playlist : NULL;
|
||||
}
|
||||
|
||||
static struct playlist *pl_user_new(const gchar *name)
|
||||
{
|
||||
struct db_entry *dbe;
|
||||
|
||||
if (db_get(&user_db, name))
|
||||
return NULL;
|
||||
dbe = db_insert(&user_db, name);
|
||||
return dbe ? &USER_PLAYLIST(dbe)->pl_playlist : NULL;
|
||||
}
|
||||
|
||||
static void pl_user_played(struct track *track)
|
||||
{
|
||||
struct db_entry *dbe, *next;
|
||||
db_for_each(dbe, next, &user_db)
|
||||
playlist_generic_update(&USER_PLAYLIST(dbe)->pl_playlist, track);
|
||||
}
|
||||
|
||||
|
||||
struct playlist_type pl_user = {
|
||||
.pl_save = pl_user_save,
|
||||
.pl_lookup = pl_user_lookup,
|
||||
.pl_get = pl_user_get,
|
||||
.pl_new = pl_user_new,
|
||||
.pl_played = pl_user_played,
|
||||
.pl_selected = pl_user_played,
|
||||
};
|
||||
|
||||
|
||||
void pl_user_init(void)
|
||||
{
|
||||
db_init(&user_db, "playlist.user", true, &user_db_ops, 0);
|
||||
db_load(&user_db);
|
||||
}
|
||||
|
||||
void pl_user_deinit()
|
||||
{
|
||||
db_deinit(&user_db);
|
||||
}
|
||||
|
||||
struct database *pl_user_db_get()
|
||||
{
|
||||
return &user_db;
|
||||
}
|
||||
|
||||
void pl_user_delete_track(struct track *track)
|
||||
{
|
||||
struct db_entry *dbe, *next;
|
||||
struct playlist *playlist;
|
||||
|
||||
db_for_each(dbe, next, &user_db) {
|
||||
playlist = &USER_PLAYLIST(dbe)->pl_playlist;
|
||||
playlist_generic_remove(playlist, track);
|
||||
}
|
||||
}
|
||||
|
||||
bool pl_user_rename(struct playlist *playlist, const gchar *name)
|
||||
{
|
||||
struct db_entry *dbe;
|
||||
|
||||
if (!playlist || db_get(&user_db, name))
|
||||
return false;
|
||||
|
||||
dbe = db_get(&user_db, playlist->pl_name);
|
||||
if (!dbe)
|
||||
return false;
|
||||
|
||||
g_free(playlist->pl_name);
|
||||
playlist->pl_name = g_strdup(name);
|
||||
db_rekey(&user_db, dbe);
|
||||
|
||||
pl_user_save();
|
||||
return true;
|
||||
}
|
234
core/queue.c
234
core/queue.c
|
@ -1,234 +0,0 @@
|
|||
/*
|
||||
* Copyright 2013 (c) Anna Schumaker.
|
||||
*/
|
||||
#include <core/queue.h>
|
||||
#include <core/random.h>
|
||||
#include <core/string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
|
||||
static int track_less_than(const void *a, const void *b, void *data)
|
||||
{
|
||||
struct track *lhs = (struct track *)a;
|
||||
struct track *rhs = (struct track *)b;
|
||||
GSList *cur = (GSList *)data;
|
||||
int res, field;
|
||||
|
||||
while (cur) {
|
||||
field = GPOINTER_TO_INT(cur->data);
|
||||
if (field > 0)
|
||||
res = track_compare(lhs, rhs, field);
|
||||
else
|
||||
res = track_compare(rhs, lhs, abs(field));
|
||||
if (res == 0) {
|
||||
cur = g_slist_next(cur);
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
};
|
||||
return res;
|
||||
}
|
||||
|
||||
static inline unsigned int __queue_added(struct queue *queue,
|
||||
struct track *track,
|
||||
unsigned int pos)
|
||||
{
|
||||
queue->q_length += track->tr_length;
|
||||
if (queue->q_ops)
|
||||
queue->q_ops->qop_added(queue, pos);
|
||||
return pos;
|
||||
}
|
||||
|
||||
static inline void __queue_remove(struct queue *queue, struct _q_iter *it)
|
||||
{
|
||||
unsigned int pos = it->it_pos;
|
||||
struct track *track = (struct track *)_q_remove_it(&queue->q_tracks, it);
|
||||
|
||||
queue->q_length -= track->tr_length;
|
||||
if (queue->q_cur.it_pos == pos)
|
||||
_q_iter_prev(&queue->q_cur);
|
||||
if (queue->q_ops)
|
||||
queue->q_ops->qop_removed(queue, pos);
|
||||
}
|
||||
|
||||
static inline void __queue_clear(struct queue *queue, unsigned int n)
|
||||
{
|
||||
queue->q_length = 0;
|
||||
if (queue->q_ops)
|
||||
queue->q_ops->qop_cleared(queue, n);
|
||||
}
|
||||
|
||||
static inline void __queue_updated(struct queue *queue, unsigned int pos)
|
||||
{
|
||||
if (queue->q_ops)
|
||||
queue->q_ops->qop_updated(queue, pos);
|
||||
}
|
||||
|
||||
static inline struct track *__queue_selected(struct queue *queue)
|
||||
{
|
||||
struct track *track = (struct track *)_q_iter_val(&queue->q_cur);
|
||||
|
||||
if (queue_has_flag(queue, Q_REPEAT) == false)
|
||||
__queue_remove(queue, &queue->q_cur);
|
||||
return track;
|
||||
}
|
||||
|
||||
static inline void __queue_save(struct queue *queue, enum queue_flags flag)
|
||||
{
|
||||
if (queue->q_ops && queue_has_flag(queue, flag))
|
||||
queue->q_ops->qop_save(queue, flag);
|
||||
}
|
||||
|
||||
void queue_init(struct queue *queue, unsigned int flags,
|
||||
const struct queue_ops *ops)
|
||||
{
|
||||
queue->q_flags = flags;
|
||||
queue->q_length = 0;
|
||||
queue->q_sort = NULL;
|
||||
queue->q_ops = ops;
|
||||
|
||||
queue->q_cur.it_pos = -1;
|
||||
queue->q_cur.it_iter = NULL;
|
||||
|
||||
_q_init(&queue->q_tracks);
|
||||
}
|
||||
|
||||
void queue_deinit(struct queue *queue)
|
||||
{
|
||||
queue_clear(queue);
|
||||
g_slist_free(queue->q_sort);
|
||||
queue->q_sort = NULL;
|
||||
}
|
||||
|
||||
void queue_set_flag(struct queue *queue, enum queue_flags flag)
|
||||
{
|
||||
queue->q_flags |= flag;
|
||||
__queue_save(queue, Q_SAVE_FLAGS);
|
||||
}
|
||||
|
||||
void queue_unset_flag(struct queue *queue, enum queue_flags flag)
|
||||
{
|
||||
queue->q_flags &= ~flag;
|
||||
__queue_save(queue, Q_SAVE_FLAGS);
|
||||
}
|
||||
|
||||
unsigned int queue_add(struct queue *queue, struct track *track)
|
||||
{
|
||||
unsigned int pos;
|
||||
|
||||
if (queue_has_flag(queue, Q_ADD_FRONT))
|
||||
pos = _q_add_head(&queue->q_tracks, track);
|
||||
else if (queue->q_sort)
|
||||
pos = _q_add_sorted(&queue->q_tracks, track,
|
||||
track_less_than, queue->q_sort);
|
||||
else
|
||||
pos = _q_add_tail(&queue->q_tracks, track);
|
||||
|
||||
return __queue_added(queue, track, pos);
|
||||
}
|
||||
|
||||
void queue_remove(struct queue *queue, unsigned int index)
|
||||
{
|
||||
struct _q_iter it;
|
||||
|
||||
_q_iter_set(&queue->q_tracks, &it, index);
|
||||
__queue_remove(queue, &it);
|
||||
}
|
||||
|
||||
void queue_remove_all(struct queue *queue, struct track *track)
|
||||
{
|
||||
struct _q_iter it;
|
||||
|
||||
while (queue_at(queue, 0) == track)
|
||||
queue_remove(queue, 0);
|
||||
|
||||
_q_for_each(&queue->q_tracks, &it) {
|
||||
if (_q_iter_val(&it) == track)
|
||||
__queue_remove(queue, &it);
|
||||
}
|
||||
}
|
||||
|
||||
void queue_clear(struct queue *queue)
|
||||
{
|
||||
unsigned int n = queue_size(queue);
|
||||
|
||||
_q_clear(&queue->q_tracks);
|
||||
__queue_clear(queue, n);
|
||||
}
|
||||
|
||||
void queue_updated(struct queue *queue, struct track *track)
|
||||
{
|
||||
struct _q_iter it;
|
||||
|
||||
_q_for_each(&queue->q_tracks, &it) {
|
||||
if (_q_iter_val(&it) == track)
|
||||
__queue_updated(queue, it.it_pos);
|
||||
}
|
||||
}
|
||||
|
||||
void queue_selected(struct queue *queue, unsigned int index)
|
||||
{
|
||||
if (queue->q_cur.it_pos != index)
|
||||
_q_iter_set(&queue->q_tracks, &queue->q_cur, index);
|
||||
__queue_selected(queue);
|
||||
}
|
||||
|
||||
struct track *queue_next(struct queue *queue)
|
||||
{
|
||||
unsigned int pos, size = queue_size(queue);
|
||||
|
||||
if (!queue_has_flag(queue, Q_ENABLED))
|
||||
return NULL;
|
||||
else if (size == 0)
|
||||
return NULL;
|
||||
|
||||
if (size == 1)
|
||||
_q_iter_set(&queue->q_tracks, &queue->q_cur, 0);
|
||||
else if (queue_has_flag(queue, Q_RANDOM)) {
|
||||
pos = queue->q_cur.it_pos + random_range(1, size / 2);
|
||||
_q_iter_set(&queue->q_tracks, &queue->q_cur, pos % size);
|
||||
} else {
|
||||
_q_iter_next(&queue->q_cur);
|
||||
if (!queue->q_cur.it_iter)
|
||||
_q_iter_set(&queue->q_tracks, &queue->q_cur, 0);
|
||||
}
|
||||
|
||||
return __queue_selected(queue);
|
||||
}
|
||||
|
||||
void queue_resort(struct queue *queue)
|
||||
{
|
||||
_q_sort(&queue->q_tracks, track_less_than, queue->q_sort);
|
||||
|
||||
for (unsigned int i = 0; i < queue_size(queue); i++)
|
||||
__queue_updated(queue, i);
|
||||
}
|
||||
|
||||
void queue_sort(struct queue *queue, enum compare_t sort, bool reset)
|
||||
{
|
||||
GSList *cur = NULL;
|
||||
int field;
|
||||
|
||||
if (queue_has_flag(queue, Q_NO_SORT))
|
||||
return;
|
||||
if (reset) {
|
||||
g_slist_free(queue->q_sort);
|
||||
queue->q_sort = NULL;
|
||||
}
|
||||
|
||||
cur = queue->q_sort;
|
||||
while (cur) {
|
||||
field = GPOINTER_TO_INT(cur->data);
|
||||
if (abs(field) == sort) {
|
||||
cur->data = GINT_TO_POINTER(-field);
|
||||
goto out_sort;
|
||||
}
|
||||
cur = g_slist_next(cur);
|
||||
}
|
||||
|
||||
queue->q_sort = g_slist_append(queue->q_sort, GINT_TO_POINTER(sort));
|
||||
|
||||
out_sort:
|
||||
queue_resort(queue);
|
||||
__queue_save(queue, Q_SAVE_SORT);
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
/*
|
||||
* Copyright 2014 (c) Anna Schumaker
|
||||
*/
|
||||
#ifdef CONFIG_TESTING
|
||||
|
||||
#include <core/random.h>
|
||||
|
||||
static unsigned int _random_value = 0;
|
||||
|
||||
|
||||
void _seed_random(unsigned int n)
|
||||
{
|
||||
_random_value = n;
|
||||
}
|
||||
|
||||
unsigned int _pick_random()
|
||||
{
|
||||
return ++_random_value;
|
||||
}
|
||||
|
||||
#endif /* CONFIG_TESTING */
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* Copyright 2015 (c) Anna Schumaker.
|
||||
*/
|
||||
#include <core/file.h>
|
||||
#include <core/settings.h>
|
||||
|
||||
static GHashTable *gui_settings = NULL;
|
||||
static struct file gui_settings_file = FILE_INIT_DATA("", "settings", 0);
|
||||
|
||||
|
||||
static void __settings_save_item(gpointer key, gpointer value, gpointer data)
|
||||
{
|
||||
file_writef(&gui_settings_file, "%s %u\n", (const gchar *)key,
|
||||
GPOINTER_TO_UINT(value));
|
||||
}
|
||||
|
||||
static void __settings_save()
|
||||
{
|
||||
file_open(&gui_settings_file, OPEN_WRITE);
|
||||
|
||||
file_writef(&gui_settings_file, "%u\n", g_hash_table_size(gui_settings));
|
||||
g_hash_table_foreach(gui_settings, __settings_save_item, NULL);
|
||||
|
||||
file_close(&gui_settings_file);
|
||||
}
|
||||
|
||||
static void __settings_read()
|
||||
{
|
||||
unsigned int num, i, value;
|
||||
gchar *key;
|
||||
|
||||
num = file_readu(&gui_settings_file);
|
||||
for (i = 0; i < num; i++) {
|
||||
key = file_readw(&gui_settings_file);
|
||||
value = file_readu(&gui_settings_file);
|
||||
|
||||
g_hash_table_insert(gui_settings, key, GUINT_TO_POINTER(value));
|
||||
}
|
||||
}
|
||||
|
||||
void settings_init()
|
||||
{
|
||||
gui_settings = g_hash_table_new_full(g_str_hash, g_str_equal,
|
||||
g_free, NULL);
|
||||
|
||||
if (file_open(&gui_settings_file, OPEN_READ))
|
||||
__settings_read();
|
||||
file_close(&gui_settings_file);
|
||||
}
|
||||
|
||||
void settings_deinit()
|
||||
{
|
||||
g_hash_table_destroy(gui_settings);
|
||||
gui_settings = NULL;
|
||||
}
|
||||
|
||||
void settings_set(const gchar *key, unsigned int value)
|
||||
{
|
||||
if (gui_settings && key) {
|
||||
g_hash_table_replace(gui_settings, g_strdup(key),
|
||||
GUINT_TO_POINTER(value));
|
||||
__settings_save();
|
||||
}
|
||||
}
|
||||
|
||||
unsigned int settings_get(const gchar *key)
|
||||
{
|
||||
if (gui_settings && key)
|
||||
return GPOINTER_TO_UINT(g_hash_table_lookup(gui_settings, key));
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool settings_has(const gchar *key)
|
||||
{
|
||||
if (gui_settings && key)
|
||||
return g_hash_table_contains(gui_settings, key);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
#ifdef CONFIG_TESTING
|
||||
GHashTable *test_get_settings()
|
||||
{
|
||||
return gui_settings;
|
||||
}
|
||||
#endif /* CONFIG_TESTING */
|
|
@ -46,51 +46,60 @@ gchar *string_tm2str(struct tm *tm)
|
|||
return buf;
|
||||
}
|
||||
|
||||
static gunichar __string_get_char(const gchar *str, const gchar *cur,
|
||||
const gchar *res)
|
||||
int string_compare_tokens(gchar **lhs, gchar **rhs)
|
||||
{
|
||||
gunichar c = g_utf8_get_char(cur);
|
||||
gchar *prev = g_utf8_find_prev_char(str, res);
|
||||
unsigned int i, cmp;
|
||||
|
||||
if (g_unichar_ismark(c))
|
||||
return '\0';
|
||||
if (g_unichar_ispunct(c))
|
||||
return '\0';
|
||||
if (g_unichar_isspace(c)) {
|
||||
if (!prev || (*prev == ' '))
|
||||
return '\0';
|
||||
return ' ';
|
||||
if (!lhs[0] && rhs[0])
|
||||
return 1;
|
||||
if (lhs[0] && !rhs[0])
|
||||
return -1;
|
||||
|
||||
for (i = 0; lhs[i]; i++) {
|
||||
if (!rhs[i])
|
||||
break;
|
||||
cmp = g_utf8_collate(lhs[i], rhs[i]);
|
||||
if (cmp != 0)
|
||||
return cmp;
|
||||
}
|
||||
return g_unichar_tolower(c);
|
||||
|
||||
if (lhs[i])
|
||||
return 1;
|
||||
if (rhs[i])
|
||||
return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
gchar *string_lowercase(const gchar *str)
|
||||
bool string_match_token(const gchar *prefix, gchar **tokens)
|
||||
{
|
||||
gchar *res = g_utf8_normalize(str, -1, G_NORMALIZE_DEFAULT);
|
||||
gchar *i, *j = res;
|
||||
gunichar c;
|
||||
unsigned int i;
|
||||
|
||||
for (i = res; *i != '\0'; i = g_utf8_next_char(i)) {
|
||||
c = __string_get_char(res, i, j);
|
||||
if (c) {
|
||||
*j = c;
|
||||
j = g_utf8_next_char(j);
|
||||
for (i = 0; tokens[i]; i++) {
|
||||
if (g_str_has_prefix(tokens[i], prefix))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool string_is_subdir(const gchar *a, const gchar *b)
|
||||
{
|
||||
gchar **parent = b ? g_strsplit(b, "/", -1) : NULL;
|
||||
gchar **child = a ? g_strsplit(a, "/", -1) : NULL;
|
||||
bool subdir = true;
|
||||
int i;
|
||||
|
||||
if (!parent || !child)
|
||||
return false;
|
||||
|
||||
for (i = 0; parent[i]; i++) {
|
||||
if (!child[i] || g_utf8_collate(parent[i], child[i]) != 0) {
|
||||
subdir = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
*j = '\0';
|
||||
|
||||
g_strchomp(res);
|
||||
return g_realloc(res, strlen(res) + 1);
|
||||
}
|
||||
|
||||
int string_compare(const gchar *lhs, const gchar *rhs)
|
||||
{
|
||||
if (strlen(lhs) == 0) {
|
||||
if (strlen(rhs) == 0)
|
||||
return 0;
|
||||
return 1;
|
||||
}
|
||||
if (strlen(rhs) == 0)
|
||||
return -1;
|
||||
return g_utf8_collate(lhs, rhs);
|
||||
g_strfreev(parent);
|
||||
g_strfreev(child);
|
||||
return subdir;
|
||||
}
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
Import("env")
|
||||
|
||||
env.UsePackage("taglib_c")
|
||||
res = Glob("*.cpp") + Glob("*.c")
|
||||
Return("res")
|
|
@ -1,84 +1,306 @@
|
|||
/*
|
||||
* Copyright 2014 (c) Anna Schumaker.
|
||||
*/
|
||||
#include <core/idle.h>
|
||||
#include <core/string.h>
|
||||
#include <core/version.h>
|
||||
#include <core/tags/album.h>
|
||||
#include <core/tags/genre.h>
|
||||
#include <coverart/caa_c.h>
|
||||
#include <musicbrainz5/mb5_c.h>
|
||||
|
||||
#ifdef CONFIG_TESTING
|
||||
#define OCARINA_AGENT "ocarina-test"
|
||||
#else
|
||||
#define OCARINA_AGENT OCARINA_NAME
|
||||
#endif
|
||||
|
||||
#define ALBUM_DB_MIN 0 /* Ocarina 6.0 */
|
||||
static struct database album_db;
|
||||
static bool album_db_upgraded = false;
|
||||
|
||||
static gchar *__album_key(const gchar *name, unsigned int year)
|
||||
struct album_cache_file {
|
||||
struct file ac_file;
|
||||
gchar *ac_subdir;
|
||||
gchar *ac_name;
|
||||
};
|
||||
|
||||
struct album_cache_file *__album_alloc_file(struct album *al)
|
||||
{
|
||||
return g_strdup_printf("%u/%s", year, name);
|
||||
struct album_cache_file *ret = g_malloc(sizeof(struct album_cache_file));
|
||||
gchar *name = g_uri_escape_string(al->al_name, " ", true);
|
||||
|
||||
ret->ac_subdir = g_strdup_printf("%d", al->al_year);
|
||||
ret->ac_name = g_strdup_printf("%s.jpg", name);
|
||||
file_init_cache(&ret->ac_file, ret->ac_subdir, ret->ac_name);
|
||||
|
||||
g_free(name);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static struct album *__album_alloc(gchar *name, unsigned int year)
|
||||
static inline void __album_free_file(struct album_cache_file *acf)
|
||||
{
|
||||
g_free(acf->ac_subdir);
|
||||
g_free(acf->ac_name);
|
||||
g_free(acf);
|
||||
}
|
||||
|
||||
static bool __album_fetch_cover(struct album *album, gchar *releaseid)
|
||||
{
|
||||
struct album_cache_file *file;
|
||||
CaaImageData image;
|
||||
CaaCoverArt *caa;
|
||||
gchar error[256];
|
||||
|
||||
caa = caa_coverart_new(OCARINA_AGENT);
|
||||
if (!caa)
|
||||
return false;
|
||||
|
||||
image = caa_coverart_fetch_front(caa, releaseid);
|
||||
if (!image) {
|
||||
caa_coverart_get_lasterrormessage(caa, error, sizeof(error));
|
||||
g_printf("Cover Art Archive: %s\n", error);
|
||||
goto out;
|
||||
}
|
||||
|
||||
file = __album_alloc_file(album);
|
||||
if (file_open(&file->ac_file, OPEN_WRITE_BINARY)) {
|
||||
file_write(&file->ac_file, caa_imagedata_data(image),
|
||||
caa_imagedata_size(image));
|
||||
file_close(&file->ac_file);
|
||||
}
|
||||
__album_free_file(file);
|
||||
|
||||
caa_imagedata_delete(image);
|
||||
out:
|
||||
caa_coverart_delete(caa);
|
||||
return album_artwork_exists(album);
|
||||
}
|
||||
|
||||
static bool __album_foreach_fetch(struct album *album, Mb5Metadata metadata)
|
||||
{
|
||||
Mb5ReleaseList list = mb5_metadata_get_releaselist(metadata);
|
||||
gchar releaseid[40];
|
||||
Mb5Release release;
|
||||
unsigned int i;
|
||||
|
||||
for (i = 0; i < mb5_release_list_size(list); i++) {
|
||||
release = mb5_release_list_item(list, i);
|
||||
if (!release)
|
||||
break;
|
||||
mb5_release_get_id(release, releaseid, sizeof(releaseid));
|
||||
if (__album_fetch_cover(album, releaseid))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool __album_run_query(struct album *album, gchar *term1,
|
||||
gchar *term2, gchar *term3)
|
||||
{
|
||||
gchar *param, *query = "query";
|
||||
Mb5Metadata data = NULL;
|
||||
unsigned int code;
|
||||
gchar error[256];
|
||||
bool ret = false;
|
||||
Mb5Query *mb5;
|
||||
|
||||
param = g_strjoin(" AND ", term1, term2, term3, NULL);
|
||||
|
||||
do {
|
||||
mb5 = mb5_query_new(OCARINA_AGENT, NULL, 0);
|
||||
if (!mb5)
|
||||
break;
|
||||
|
||||
data = mb5_query_query(mb5, "release", "", "", 1, &query, ¶m);
|
||||
code = mb5_query_get_lasthttpcode(mb5);
|
||||
if (mb5_query_get_lastresult(mb5) != 0) {
|
||||
mb5_query_get_lasterrormessage(mb5, error, sizeof(error));
|
||||
g_printf("MusicBrainz: %s\n", error);
|
||||
}
|
||||
|
||||
mb5_query_delete(mb5);
|
||||
} while (code == 503);
|
||||
|
||||
if (data) {
|
||||
ret = __album_foreach_fetch(album, data);
|
||||
mb5_metadata_delete(data);
|
||||
}
|
||||
|
||||
g_free(param);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static bool __album_query_artist(struct album *album, struct artist *al_artist,
|
||||
gchar *lower)
|
||||
{
|
||||
gchar *release, *artist, *year;
|
||||
bool found = false;
|
||||
|
||||
if (!al_artist || !string_length(al_artist->ar_name) ||
|
||||
strcmp(al_artist->ar_tokens[0], "various") == 0)
|
||||
return false;
|
||||
|
||||
release = g_strdup_printf("release:\"%s\"~", lower);
|
||||
artist = g_strdup_printf("artist:\"%s\"~", al_artist->ar_name);
|
||||
year = g_strdup_printf("date:%d*", album->al_year);
|
||||
|
||||
if (album->al_year > 0)
|
||||
found = __album_run_query(album, release, artist, year);
|
||||
if (!found)
|
||||
found = __album_run_query(album, release, artist, NULL);
|
||||
if (!found && album->al_year > 0)
|
||||
found = __album_run_query(album, lower, artist, year);
|
||||
if (!found)
|
||||
found = __album_run_query(album, lower, artist, NULL);
|
||||
|
||||
g_free(release);
|
||||
g_free(artist);
|
||||
g_free(year);
|
||||
return found;
|
||||
}
|
||||
|
||||
static bool __album_fetch_artwork(struct album *album)
|
||||
{
|
||||
gchar *lower;
|
||||
|
||||
if (album_artwork_exists(album))
|
||||
return true;
|
||||
if (string_length(album->al_name) == 0)
|
||||
return true;
|
||||
|
||||
lower = g_strjoinv(" ", album->al_tokens);
|
||||
if (!__album_query_artist(album, album->al_artist, lower))
|
||||
__album_run_query(album, lower, NULL, NULL);
|
||||
|
||||
g_free(lower);
|
||||
return true;
|
||||
}
|
||||
|
||||
static gchar *__album_key(struct artist *artist, struct genre *genre,
|
||||
const gchar *name, unsigned int year)
|
||||
{
|
||||
if (!artist || !genre)
|
||||
return g_strdup_printf("%u/%s", year, name);
|
||||
return g_strdup_printf("%u/%u/%u/%s", artist_index(artist),
|
||||
genre_index(genre), year, name);
|
||||
}
|
||||
|
||||
static struct album *__album_alloc(struct artist *artist, struct genre *genre,
|
||||
gchar *name, unsigned int year)
|
||||
{
|
||||
struct album *album = g_malloc(sizeof(struct album));
|
||||
|
||||
dbe_init(&album->al_dbe, album);
|
||||
album->al_year = year;
|
||||
album->al_name = name;
|
||||
album->al_lower = string_lowercase(album->al_name);
|
||||
album->al_year = year;
|
||||
album->al_name = name;
|
||||
album->al_tokens = g_str_tokenize_and_fold(name, NULL, &album->al_alts);
|
||||
album->al_artist = artist;
|
||||
album->al_genre = genre;
|
||||
|
||||
if (!album_artwork_exists(album) && artist && genre)
|
||||
idle_schedule(IDLE_ASYNC, IDLE_FUNC(__album_fetch_artwork), album);
|
||||
return album;
|
||||
}
|
||||
|
||||
|
||||
static struct db_entry *album_alloc(const gchar *key)
|
||||
static struct db_entry *__album_alloc_v0(const gchar *key)
|
||||
{
|
||||
unsigned int year;
|
||||
gchar *name;
|
||||
|
||||
if (sscanf(key, "%u/%m[^\n]", &year, &name) == 1)
|
||||
name = g_strdup("");
|
||||
return &__album_alloc(name, year)->al_dbe;
|
||||
return &__album_alloc(NULL, NULL, name, year)->al_dbe;
|
||||
}
|
||||
|
||||
static struct db_entry *album_alloc(const gchar *key, unsigned int index)
|
||||
{
|
||||
unsigned int artist_id, genre_id, year, n;
|
||||
gchar *name;
|
||||
|
||||
n = sscanf(key, "%u/%u/%u/%m[^\n]", &artist_id, &genre_id, &year, &name);
|
||||
if (n == 1)
|
||||
return __album_alloc_v0(key);
|
||||
else if (n == 3)
|
||||
name = g_strdup("");
|
||||
return &__album_alloc(artist_get(artist_id), genre_get(genre_id),
|
||||
name, year)->al_dbe;
|
||||
}
|
||||
|
||||
static void album_free(struct db_entry *dbe)
|
||||
{
|
||||
g_free(ALBUM(dbe)->al_name);
|
||||
g_free(ALBUM(dbe)->al_lower);
|
||||
g_strfreev(ALBUM(dbe)->al_tokens);
|
||||
g_strfreev(ALBUM(dbe)->al_alts);
|
||||
g_free(ALBUM(dbe));
|
||||
}
|
||||
|
||||
static gchar *album_key(struct db_entry *dbe)
|
||||
{
|
||||
return __album_key(ALBUM(dbe)->al_name, ALBUM(dbe)->al_year);
|
||||
return __album_key(ALBUM(dbe)->al_artist, ALBUM(dbe)->al_genre,
|
||||
ALBUM(dbe)->al_name, ALBUM(dbe)->al_year);
|
||||
}
|
||||
|
||||
static struct db_entry *album_read(struct file *file)
|
||||
static struct album *__album_parse_v0(gchar *line)
|
||||
{
|
||||
unsigned int year;
|
||||
gchar *name;
|
||||
|
||||
if (sscanf(line, "%u %m[^\n]", &year, &name) == 1)
|
||||
name = g_strdup("");
|
||||
return __album_alloc(NULL, NULL, name, year);
|
||||
}
|
||||
|
||||
static struct db_entry *album_read(struct file *file, unsigned int index)
|
||||
{
|
||||
unsigned int year, artist_id, genre_id, n;
|
||||
struct album *album;
|
||||
gchar *line, *name;
|
||||
|
||||
line = file_readl(file);
|
||||
if (sscanf(line, "%u %m[^\n]", &year, &name) == 1)
|
||||
if (file_version(file) == 0) {
|
||||
album = __album_parse_v0(line);
|
||||
album_db_upgraded = true;
|
||||
goto out;
|
||||
}
|
||||
|
||||
n = sscanf(line, "%u %u %u %m[^\n]", &artist_id, &genre_id, &year, &name);
|
||||
if (n == 3)
|
||||
name = g_strdup("");
|
||||
|
||||
album = __album_alloc(artist_get(artist_id),
|
||||
genre_get(genre_id), name, year);
|
||||
out:
|
||||
g_free(line);
|
||||
return &__album_alloc(name, year)->al_dbe;
|
||||
return &album->al_dbe;
|
||||
}
|
||||
|
||||
static void album_write(struct file *file, struct db_entry *dbe)
|
||||
{
|
||||
file_writef(file, "%u %s", ALBUM(dbe)->al_year, ALBUM(dbe)->al_name);
|
||||
struct album *album = ALBUM(dbe);
|
||||
struct artist *artist = album->al_artist;
|
||||
struct genre *genre = album->al_genre;
|
||||
|
||||
file_writef(file, "%u %u %u %s", artist ? artist_index(artist) : 0,
|
||||
genre ? genre_index(genre) : 0,
|
||||
album->al_year, album->al_name);
|
||||
}
|
||||
|
||||
|
||||
static const struct db_ops album_ops = {
|
||||
album_alloc,
|
||||
album_free,
|
||||
album_key,
|
||||
album_read,
|
||||
NULL,
|
||||
album_write,
|
||||
.dbe_alloc = album_alloc,
|
||||
.dbe_free = album_free,
|
||||
.dbe_key = album_key,
|
||||
.dbe_read = album_read,
|
||||
.dbe_write = album_write,
|
||||
};
|
||||
|
||||
|
||||
void album_db_init()
|
||||
{
|
||||
db_init(&album_db, "album.db", true, &album_ops);
|
||||
db_init(&album_db, "album.db", true, &album_ops, ALBUM_DB_MIN);
|
||||
db_load(&album_db);
|
||||
}
|
||||
|
||||
|
@ -87,9 +309,30 @@ void album_db_deinit()
|
|||
db_deinit(&album_db);
|
||||
}
|
||||
|
||||
struct album *album_find(const gchar *name, unsigned int year)
|
||||
bool album_db_defrag()
|
||||
{
|
||||
gchar *key = __album_key(name, year);
|
||||
return db_defrag(&album_db);
|
||||
}
|
||||
|
||||
bool album_db_upgrade_done()
|
||||
{
|
||||
struct db_entry *dbe, *next;
|
||||
|
||||
if (album_db_upgraded == false)
|
||||
return false;
|
||||
|
||||
db_for_each(dbe, next, &album_db) {
|
||||
if (!ALBUM(dbe)->al_artist && !ALBUM(dbe)->al_genre)
|
||||
db_remove(&album_db, dbe);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
struct album *album_find(struct artist *artist, struct genre *genre,
|
||||
const gchar *name, unsigned int year)
|
||||
{
|
||||
gchar *key = __album_key(artist, genre, name, year);
|
||||
struct album *album = ALBUM(db_find(&album_db, key));
|
||||
g_free(key);
|
||||
return album;
|
||||
|
@ -102,7 +345,7 @@ struct album *album_get(const unsigned int index)
|
|||
|
||||
int album_compare(struct album *lhs, struct album *rhs)
|
||||
{
|
||||
return string_compare(lhs->al_lower, rhs->al_lower);
|
||||
return string_compare_tokens(lhs->al_tokens, rhs->al_tokens);
|
||||
}
|
||||
|
||||
int album_compare_year(struct album *lhs, struct album *rhs)
|
||||
|
@ -112,6 +355,52 @@ int album_compare_year(struct album *lhs, struct album *rhs)
|
|||
return lhs->al_year - rhs->al_year;
|
||||
}
|
||||
|
||||
bool album_match_token(struct album *album, const gchar *string)
|
||||
{
|
||||
return string_match_token(string, album->al_tokens) ||
|
||||
string_match_token(string, album->al_alts);
|
||||
}
|
||||
|
||||
bool album_artwork_exists(struct album *album)
|
||||
{
|
||||
struct album_cache_file *file;
|
||||
bool ret;
|
||||
|
||||
file = __album_alloc_file(album);
|
||||
ret = file_exists(&file->ac_file);
|
||||
__album_free_file(file);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
gchar *album_artwork_path(struct album *album)
|
||||
{
|
||||
struct album_cache_file *file;
|
||||
gchar *ret = NULL;
|
||||
|
||||
file = __album_alloc_file(album);
|
||||
if (file_exists(&file->ac_file))
|
||||
ret = file_path(&file->ac_file);
|
||||
__album_free_file(file);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool album_artwork_import(struct album *album, gchar *path)
|
||||
{
|
||||
struct album_cache_file *file;
|
||||
bool ret = false;
|
||||
|
||||
file = __album_alloc_file(album);
|
||||
if (path && file_open(&file->ac_file, OPEN_WRITE_BINARY)) {
|
||||
ret = file_import(&file->ac_file, path);
|
||||
file_close(&file->ac_file);
|
||||
}
|
||||
__album_free_file(file);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
#ifdef CONFIG_TESTING
|
||||
const struct db_ops *test_album_ops() { return &album_ops; }
|
||||
#endif /* CONFIG_TESTING */
|
||||
|
|
|
@ -12,21 +12,23 @@ static struct artist *__artist_alloc(gchar *name)
|
|||
struct artist *artist = g_malloc(sizeof(struct artist));
|
||||
|
||||
dbe_init(&artist->ar_dbe, artist);
|
||||
artist->ar_name = name;
|
||||
artist->ar_lower = string_lowercase(name);
|
||||
artist->ar_name = name;
|
||||
artist->ar_tokens = g_str_tokenize_and_fold(name, NULL, &artist->ar_alts);
|
||||
artist->ar_playlist = NULL;
|
||||
|
||||
return artist;
|
||||
}
|
||||
|
||||
|
||||
static struct db_entry *artist_alloc(const gchar *name)
|
||||
static struct db_entry *artist_alloc(const gchar *name, unsigned int index)
|
||||
{
|
||||
return &__artist_alloc(g_strdup(name))->ar_dbe;
|
||||
}
|
||||
|
||||
static void artist_free(struct db_entry *dbe)
|
||||
{
|
||||
g_free(ARTIST(dbe)->ar_lower);
|
||||
g_strfreev(ARTIST(dbe)->ar_tokens);
|
||||
g_strfreev(ARTIST(dbe)->ar_alts);
|
||||
g_free(ARTIST(dbe));
|
||||
}
|
||||
|
||||
|
@ -35,7 +37,7 @@ static gchar *artist_key(struct db_entry *dbe)
|
|||
return ARTIST(dbe)->ar_name;
|
||||
}
|
||||
|
||||
struct db_entry *artist_read(struct file *file)
|
||||
struct db_entry *artist_read(struct file *file, unsigned int index)
|
||||
{
|
||||
return &__artist_alloc(file_readl(file))->ar_dbe;
|
||||
}
|
||||
|
@ -47,18 +49,17 @@ static void artist_write(struct file *file, struct db_entry *dbe)
|
|||
|
||||
|
||||
static const struct db_ops artist_ops = {
|
||||
artist_alloc,
|
||||
artist_free,
|
||||
artist_key,
|
||||
artist_read,
|
||||
NULL,
|
||||
artist_write,
|
||||
.dbe_alloc = artist_alloc,
|
||||
.dbe_free = artist_free,
|
||||
.dbe_key = artist_key,
|
||||
.dbe_read = artist_read,
|
||||
.dbe_write = artist_write,
|
||||
};
|
||||
|
||||
|
||||
void artist_db_init()
|
||||
{
|
||||
db_init(&artist_db, "artist.db", true, &artist_ops);
|
||||
db_init(&artist_db, "artist.db", true, &artist_ops, 0);
|
||||
db_load(&artist_db);
|
||||
}
|
||||
|
||||
|
@ -67,11 +68,21 @@ void artist_db_deinit()
|
|||
db_deinit(&artist_db);
|
||||
}
|
||||
|
||||
const struct database *artist_db_get()
|
||||
{
|
||||
return &artist_db;
|
||||
}
|
||||
|
||||
struct artist *artist_find(const gchar *name)
|
||||
{
|
||||
return ARTIST(db_find(&artist_db, name));
|
||||
}
|
||||
|
||||
struct artist *artist_lookup(const gchar *name)
|
||||
{
|
||||
return ARTIST(db_get(&artist_db, name));
|
||||
}
|
||||
|
||||
struct artist *artist_get(const unsigned int index)
|
||||
{
|
||||
return ARTIST(db_at(&artist_db, index));
|
||||
|
@ -79,9 +90,14 @@ struct artist *artist_get(const unsigned int index)
|
|||
|
||||
int artist_compare(struct artist *lhs, struct artist *rhs)
|
||||
{
|
||||
return string_compare(lhs->ar_lower, rhs->ar_lower);
|
||||
return string_compare_tokens(lhs->ar_tokens, rhs->ar_tokens);
|
||||
}
|
||||
|
||||
bool artist_match_token(struct artist *artist, const gchar *string)
|
||||
{
|
||||
return string_match_token(string, artist->ar_tokens) ||
|
||||
string_match_token(string, artist->ar_alts);
|
||||
}
|
||||
#ifdef CONFIG_TESTING
|
||||
const struct db_ops *test_artist_ops() { return &artist_ops; }
|
||||
#endif /* CONFIG_TESTING */
|
||||
|
|
|
@ -12,20 +12,21 @@ static struct genre *__genre_alloc(gchar *name)
|
|||
struct genre *genre = g_malloc(sizeof(struct genre));
|
||||
|
||||
dbe_init(&genre->ge_dbe, genre);
|
||||
genre->ge_name = name;
|
||||
genre->ge_lower = string_lowercase(name);
|
||||
genre->ge_name = name;
|
||||
genre->ge_tokens = g_str_tokenize_and_fold(name, NULL, &genre->ge_alts);
|
||||
|
||||
return genre;
|
||||
}
|
||||
|
||||
static struct db_entry *genre_alloc(const gchar *name)
|
||||
static struct db_entry *genre_alloc(const gchar *name, unsigned int index)
|
||||
{
|
||||
return &__genre_alloc(g_strdup(name))->ge_dbe;
|
||||
}
|
||||
|
||||
static void genre_free(struct db_entry *dbe)
|
||||
{
|
||||
g_free(GENRE(dbe)->ge_lower);
|
||||
g_strfreev(GENRE(dbe)->ge_tokens);
|
||||
g_strfreev(GENRE(dbe)->ge_alts);
|
||||
g_free(GENRE(dbe));
|
||||
}
|
||||
|
||||
|
@ -34,7 +35,7 @@ static gchar *genre_key(struct db_entry *dbe)
|
|||
return GENRE(dbe)->ge_name;
|
||||
}
|
||||
|
||||
static struct db_entry *genre_read(struct file *file)
|
||||
static struct db_entry *genre_read(struct file *file, unsigned int index)
|
||||
{
|
||||
return &__genre_alloc(file_readl(file))->ge_dbe;
|
||||
}
|
||||
|
@ -46,18 +47,17 @@ static void genre_write(struct file *file, struct db_entry *dbe)
|
|||
|
||||
|
||||
static const struct db_ops genre_ops = {
|
||||
genre_alloc,
|
||||
genre_free,
|
||||
genre_key,
|
||||
genre_read,
|
||||
NULL,
|
||||
genre_write,
|
||||
.dbe_alloc = genre_alloc,
|
||||
.dbe_free = genre_free,
|
||||
.dbe_key = genre_key,
|
||||
.dbe_read = genre_read,
|
||||
.dbe_write = genre_write,
|
||||
};
|
||||
|
||||
|
||||
void genre_db_init()
|
||||
{
|
||||
db_init(&genre_db, "genre.db", true, &genre_ops);
|
||||
db_init(&genre_db, "genre.db", true, &genre_ops, 0);
|
||||
db_load(&genre_db);
|
||||
}
|
||||
|
||||
|
@ -78,7 +78,13 @@ struct genre *genre_get(const unsigned int index)
|
|||
|
||||
int genre_compare(struct genre *lhs, struct genre *rhs)
|
||||
{
|
||||
return string_compare(lhs->ge_lower, rhs->ge_lower);
|
||||
return string_compare_tokens(lhs->ge_tokens, rhs->ge_tokens);
|
||||
}
|
||||
|
||||
bool genre_match_token(struct genre *genre, const gchar *string)
|
||||
{
|
||||
return string_match_token(string, genre->ge_tokens) ||
|
||||
string_match_token(string, genre->ge_alts);
|
||||
}
|
||||
|
||||
#ifdef CONFIG_TESTING
|
||||
|
|
|
@ -1,26 +1,26 @@
|
|||
/*
|
||||
* Copyright 2014 (c) Anna Schumaker.
|
||||
*/
|
||||
#include <core/string.h>
|
||||
#include <core/tags/library.h>
|
||||
|
||||
|
||||
#define LIBRARY_DB_MIN 0 /* Ocarina 6.0 */
|
||||
static struct database library_db;
|
||||
|
||||
static struct library *__library_alloc(gchar *path, bool enabled)
|
||||
static struct library *__library_alloc(gchar *path)
|
||||
{
|
||||
struct library *library = g_malloc(sizeof(struct library));
|
||||
|
||||
dbe_init(&library->li_dbe, library);
|
||||
library->li_size = 0;
|
||||
library->li_enabled = enabled;
|
||||
library->li_path = path;
|
||||
library->li_path = path;
|
||||
library->li_playlist = NULL;
|
||||
|
||||
return library;
|
||||
}
|
||||
|
||||
static struct db_entry *library_alloc(const gchar *path)
|
||||
static struct db_entry *library_alloc(const gchar *path, unsigned int index)
|
||||
{
|
||||
return &__library_alloc(g_strdup(path), true)->li_dbe;
|
||||
return &__library_alloc(g_strdup(path))->li_dbe;
|
||||
}
|
||||
|
||||
static void library_free(struct db_entry *dbe)
|
||||
|
@ -33,35 +33,36 @@ static gchar *library_key(struct db_entry *dbe)
|
|||
return LIBRARY(dbe)->li_path;
|
||||
}
|
||||
|
||||
static struct db_entry *library_read(struct file *file)
|
||||
static struct db_entry *library_read(struct file *file, unsigned int index)
|
||||
{
|
||||
int enabled;
|
||||
gchar *path;
|
||||
|
||||
file_readf(file, "%d %m[^\n]", &enabled, &path);
|
||||
return &__library_alloc(path, enabled)->li_dbe;
|
||||
/* Old "enabled" flag */
|
||||
if (file_version(file) == 0)
|
||||
file_readd(file);
|
||||
|
||||
path = file_readl(file);
|
||||
return &__library_alloc(path)->li_dbe;
|
||||
}
|
||||
|
||||
static void library_write(struct file *file, struct db_entry *dbe)
|
||||
{
|
||||
file_writef(file, "%d %s", LIBRARY(dbe)->li_enabled,
|
||||
LIBRARY(dbe)->li_path);
|
||||
file_writef(file, "%s", LIBRARY(dbe)->li_path);
|
||||
}
|
||||
|
||||
|
||||
static const struct db_ops library_ops = {
|
||||
library_alloc,
|
||||
library_free,
|
||||
library_key,
|
||||
library_read,
|
||||
NULL,
|
||||
library_write,
|
||||
.dbe_alloc = library_alloc,
|
||||
.dbe_free = library_free,
|
||||
.dbe_key = library_key,
|
||||
.dbe_read = library_read,
|
||||
.dbe_write = library_write,
|
||||
};
|
||||
|
||||
|
||||
void library_db_init()
|
||||
{
|
||||
db_init(&library_db, "library.db", true, &library_ops);
|
||||
db_init(&library_db, "library.db", true, &library_ops, LIBRARY_DB_MIN);
|
||||
db_load(&library_db);
|
||||
}
|
||||
|
||||
|
@ -70,6 +71,11 @@ void library_db_deinit()
|
|||
db_deinit(&library_db);
|
||||
}
|
||||
|
||||
bool library_db_defrag()
|
||||
{
|
||||
return db_defrag(&library_db);
|
||||
}
|
||||
|
||||
const struct database *library_db_get()
|
||||
{
|
||||
return &library_db;
|
||||
|
@ -77,7 +83,22 @@ const struct database *library_db_get()
|
|||
|
||||
struct library *library_find(const gchar *path)
|
||||
{
|
||||
return LIBRARY(db_find(&library_db, path));
|
||||
struct library *library = library_lookup(path);
|
||||
if (library)
|
||||
return library;
|
||||
return LIBRARY(db_insert(&library_db, path));
|
||||
}
|
||||
|
||||
struct library *library_lookup(const gchar *path)
|
||||
{
|
||||
struct db_entry *dbe, *next;
|
||||
|
||||
db_for_each(dbe, next, &library_db) {
|
||||
if (string_is_subdir(path, LIBRARY(dbe)->li_path))
|
||||
return LIBRARY(dbe);
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
struct library *library_get(const unsigned int index)
|
||||
|
@ -91,12 +112,6 @@ void library_remove(struct library *library)
|
|||
db_remove(&library_db, &library->li_dbe);
|
||||
}
|
||||
|
||||
void library_set_enabled(struct library *library, bool enabled)
|
||||
{
|
||||
library->li_enabled = enabled;
|
||||
db_save(&library_db);
|
||||
}
|
||||
|
||||
gchar *library_file(struct library *library, const gchar *path)
|
||||
{
|
||||
return g_strdup_printf("%s/%s", library->li_path, path);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
* Copyright 2014 (c) Anna Schumaker.
|
||||
*/
|
||||
#include <core/idle.h>
|
||||
#include <core/tags/album.h>
|
||||
#include <core/tags/artist.h>
|
||||
#include <core/tags/genre.h>
|
||||
|
@ -8,13 +9,22 @@
|
|||
#include <core/tags/tags.h>
|
||||
#include <core/tags/track.h>
|
||||
|
||||
static bool tags_upgrade_done(void *data)
|
||||
{
|
||||
if (album_db_upgrade_done())
|
||||
track_db_commit();
|
||||
return true;
|
||||
}
|
||||
|
||||
void tags_init()
|
||||
{
|
||||
album_db_init();
|
||||
artist_db_init();
|
||||
genre_db_init();
|
||||
album_db_init();
|
||||
library_db_init();
|
||||
track_db_init();
|
||||
|
||||
idle_schedule(IDLE_SYNC, tags_upgrade_done, NULL);
|
||||
}
|
||||
|
||||
void tags_deinit()
|
||||
|
@ -22,6 +32,19 @@ void tags_deinit()
|
|||
track_db_deinit();
|
||||
library_db_deinit();
|
||||
genre_db_deinit();
|
||||
artist_db_deinit();
|
||||
album_db_deinit();
|
||||
artist_db_deinit();
|
||||
}
|
||||
|
||||
bool tags_defragment(void *data)
|
||||
{
|
||||
bool album = album_db_defrag();
|
||||
bool library = library_db_defrag();
|
||||
bool track = track_db_defrag();
|
||||
|
||||
if (library)
|
||||
track_db_rekey();
|
||||
if (album || library || track)
|
||||
track_db_commit();
|
||||
return track;
|
||||
}
|
||||
|
|
|
@ -1,22 +1,23 @@
|
|||
/*
|
||||
* Copyright 2014 (c) Anna Schumaker.
|
||||
*/
|
||||
#include <core/filter.h>
|
||||
#include <core/string.h>
|
||||
#include <core/tags/track.h>
|
||||
|
||||
#include <glib.h>
|
||||
#include <taglib/tag_c.h>
|
||||
|
||||
|
||||
#define TRACK_DB_MIN 0 /* Ocarina 6.0 */
|
||||
static struct database track_db;
|
||||
static unsigned int unplayed_count = 0;
|
||||
static unsigned int play_count = 0;
|
||||
|
||||
static gchar *__track_key(struct library *library, gchar *path)
|
||||
{
|
||||
gchar *res;
|
||||
|
||||
if (library)
|
||||
res = g_strdup_printf("%u/%s", library->li_dbe.dbe_index, path);
|
||||
res = g_strdup_printf("%u/%s", library_index(library), path);
|
||||
else
|
||||
res = g_strdup("");
|
||||
|
||||
|
@ -40,47 +41,70 @@ static struct track *__track_alloc()
|
|||
return track;
|
||||
}
|
||||
|
||||
|
||||
struct db_entry *track_alloc(const gchar *key)
|
||||
static struct track *__track_alloc_filepath(const gchar *filepath)
|
||||
{
|
||||
TagLib_File *file = taglib_file_new(filepath);
|
||||
const TagLib_AudioProperties *audio;
|
||||
struct library *library;
|
||||
struct track *track = NULL;
|
||||
unsigned int lib_id;
|
||||
TagLib_File *file;
|
||||
TagLib_Tag *tag;
|
||||
char *fullpath, *path;
|
||||
struct artist *artist;
|
||||
struct genre *genre;
|
||||
TagLib_Tag *tag;
|
||||
|
||||
sscanf(key, "%u/%m[^\n]", &lib_id, &path);
|
||||
library = library_get(lib_id);
|
||||
fullpath = library_file(library, path);
|
||||
file = taglib_file_new(fullpath);
|
||||
if (!file || !taglib_file_is_valid(file)) {
|
||||
printf("WARNING: Could not read tags for: %s\n", fullpath);
|
||||
goto out;
|
||||
g_printerr("WARNING: Could not read tags for: %s\n", filepath);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
track = __track_alloc();
|
||||
tag = taglib_file_tag(file);
|
||||
audio = taglib_file_audioproperties(file);
|
||||
track = __track_alloc();
|
||||
tag = taglib_file_tag(file);
|
||||
audio = taglib_file_audioproperties(file);
|
||||
artist = artist_find(taglib_tag_artist(tag));
|
||||
genre = genre_find(taglib_tag_genre(tag));
|
||||
|
||||
track->tr_album = album_find(taglib_tag_album(tag), taglib_tag_year(tag));
|
||||
track->tr_artist = artist_find(taglib_tag_artist(tag));
|
||||
track->tr_genre = genre_find(taglib_tag_genre(tag));
|
||||
track->tr_library = library;
|
||||
track->tr_album = album_find(artist, genre, taglib_tag_album(tag),
|
||||
taglib_tag_year(tag));
|
||||
|
||||
track->tr_count = 0;
|
||||
track->tr_length = taglib_audioproperties_length(audio);
|
||||
track->tr_track = taglib_tag_track(tag);
|
||||
date_set(&track->tr_date, 0, 0, 0);
|
||||
|
||||
track->tr_path = g_strdup(key);
|
||||
track->tr_title = g_strdup(taglib_tag_title(tag));
|
||||
track->tr_lower = string_lowercase(track->tr_title);
|
||||
track->tr_title = g_strdup(taglib_tag_title(tag));
|
||||
track->tr_tokens = g_str_tokenize_and_fold(track->tr_title, NULL,
|
||||
&track->tr_alts);
|
||||
|
||||
taglib_tag_free_strings();
|
||||
taglib_file_free(file);
|
||||
out:
|
||||
return track;
|
||||
}
|
||||
|
||||
static void __track_free(struct track *track)
|
||||
{
|
||||
g_strfreev(track->tr_tokens);
|
||||
g_strfreev(track->tr_alts);
|
||||
g_free(track->tr_title);
|
||||
g_free(track);
|
||||
}
|
||||
|
||||
|
||||
struct db_entry *track_alloc(const gchar *key, unsigned int index)
|
||||
{
|
||||
struct library *library;
|
||||
char *fullpath, *path;
|
||||
struct track *track;
|
||||
unsigned int lib_id;
|
||||
|
||||
sscanf(key, "%u/%m[^\n]", &lib_id, &path);
|
||||
library = library_get(lib_id);
|
||||
fullpath = library_file(library, path);
|
||||
track = __track_alloc_filepath(fullpath);
|
||||
|
||||
if (track) {
|
||||
track->tr_library = library;
|
||||
track->tr_path = g_strdup(key);
|
||||
unplayed_count++;
|
||||
}
|
||||
|
||||
g_free(path);
|
||||
g_free(fullpath);
|
||||
return track ? &track->tr_dbe : NULL;
|
||||
|
@ -90,22 +114,11 @@ static void track_free(struct db_entry *dbe)
|
|||
{
|
||||
struct track *track = TRACK(dbe);
|
||||
|
||||
g_free(track->tr_title);
|
||||
g_free(track->tr_lower);
|
||||
play_count -= track->tr_count;
|
||||
if (track->tr_count == 0)
|
||||
unplayed_count--;
|
||||
|
||||
if (track->tr_library)
|
||||
track->tr_library->li_size--;
|
||||
g_free(track);
|
||||
}
|
||||
|
||||
static void track_setup(struct db_entry *dbe)
|
||||
{
|
||||
struct track *track = TRACK(dbe);
|
||||
|
||||
filter_add(track->tr_lower, dbe->dbe_index);
|
||||
filter_add(track->tr_artist->ar_lower, dbe->dbe_index);
|
||||
filter_add(track->tr_album->al_lower, dbe->dbe_index);
|
||||
track->tr_library->li_size++;
|
||||
__track_free(track);
|
||||
}
|
||||
|
||||
static gchar *track_key(struct db_entry *dbe)
|
||||
|
@ -113,25 +126,48 @@ static gchar *track_key(struct db_entry *dbe)
|
|||
return TRACK(dbe)->tr_path;
|
||||
}
|
||||
|
||||
static struct db_entry *track_read(struct file *file)
|
||||
static void track_read_v0(struct file *file, struct track *track)
|
||||
{
|
||||
struct artist *artist = artist_get(file_readu(file));
|
||||
struct album *album = album_get( file_readu(file));
|
||||
struct genre *genre = genre_get( file_readu(file));
|
||||
|
||||
track->tr_track = file_readhu(file);
|
||||
date_read(file, &track->tr_date);
|
||||
|
||||
if (album->al_artist != artist || album->al_genre != genre)
|
||||
album = album_find(artist, genre, album->al_name, album->al_year);
|
||||
|
||||
track->tr_album = album;
|
||||
}
|
||||
|
||||
static struct db_entry *track_read(struct file *file, unsigned int index)
|
||||
{
|
||||
unsigned int library_id, artist_id, album_id, genre_id;
|
||||
struct track *track = __track_alloc();
|
||||
|
||||
file_readf(file, "%u %u %u %u %u", &library_id, &artist_id, &album_id,
|
||||
&genre_id, &track->tr_track);
|
||||
date_read(file, &track->tr_date);
|
||||
file_readf(file, "%u %u", &track->tr_count, &track->tr_length);
|
||||
track->tr_library = library_get(file_readu(file));
|
||||
|
||||
track->tr_album = album_get(album_id);
|
||||
track->tr_artist = artist_get(artist_id);
|
||||
track->tr_genre = genre_get(genre_id);
|
||||
track->tr_library = library_get(library_id);
|
||||
if (file_version(file) == 0)
|
||||
track_read_v0(file, track);
|
||||
else {
|
||||
track->tr_album = album_get(file_readu(file));
|
||||
track->tr_track = file_readhu(file);
|
||||
date_read_stamp(file, &track->tr_date);
|
||||
}
|
||||
|
||||
track->tr_title = file_readl(file);
|
||||
track->tr_lower = string_lowercase(track->tr_title);
|
||||
track->tr_count = file_readhu(file);
|
||||
track->tr_length = file_readhu(file);
|
||||
|
||||
play_count += track->tr_count;
|
||||
if (track->tr_count == 0)
|
||||
unplayed_count++;
|
||||
|
||||
track->tr_title = file_readl(file);
|
||||
track->tr_tokens = g_str_tokenize_and_fold(track->tr_title, NULL,
|
||||
&track->tr_alts);
|
||||
track->tr_path = __track_key(track->tr_library, file_readl(file));
|
||||
return &track->tr_dbe;
|
||||
|
||||
}
|
||||
|
||||
static void track_write(struct file *file, struct db_entry *dbe)
|
||||
|
@ -139,13 +175,11 @@ static void track_write(struct file *file, struct db_entry *dbe)
|
|||
struct track *track = TRACK(dbe);
|
||||
gchar *path = __track_path(track);
|
||||
|
||||
file_writef(file, "%u %u %u %u %u ", track->tr_library->li_dbe.dbe_index,
|
||||
track->tr_artist->ar_dbe.dbe_index,
|
||||
track->tr_album->al_dbe.dbe_index,
|
||||
track->tr_genre->ge_dbe.dbe_index,
|
||||
track->tr_track);
|
||||
date_write(file, &track->tr_date);
|
||||
file_writef(file, " %u %u %s\n%s\n", track->tr_count,
|
||||
file_writef(file, "%u %u %hu ", library_index(track->tr_library),
|
||||
album_index(track->tr_album),
|
||||
track->tr_track);
|
||||
date_write_stamp(file, &track->tr_date);
|
||||
file_writef(file, " %hu %hu %s\n%s", track->tr_count,
|
||||
track->tr_length,
|
||||
track->tr_title,
|
||||
path);
|
||||
|
@ -155,18 +189,17 @@ static void track_write(struct file *file, struct db_entry *dbe)
|
|||
|
||||
|
||||
static const struct db_ops track_ops = {
|
||||
track_alloc,
|
||||
track_free,
|
||||
track_key,
|
||||
track_read,
|
||||
track_setup,
|
||||
track_write,
|
||||
.dbe_alloc = track_alloc,
|
||||
.dbe_free = track_free,
|
||||
.dbe_key = track_key,
|
||||
.dbe_read = track_read,
|
||||
.dbe_write = track_write,
|
||||
};
|
||||
|
||||
|
||||
void track_db_init()
|
||||
{
|
||||
db_init(&track_db, "track.db", false, &track_ops);
|
||||
db_init(&track_db, "track.db", false, &track_ops, TRACK_DB_MIN);
|
||||
db_load(&track_db);
|
||||
}
|
||||
|
||||
|
@ -180,11 +213,70 @@ void track_db_commit()
|
|||
db_save(&track_db);
|
||||
}
|
||||
|
||||
bool track_db_defrag()
|
||||
{
|
||||
return db_defrag(&track_db);
|
||||
}
|
||||
|
||||
void track_db_rekey()
|
||||
{
|
||||
struct db_entry *dbe, *next;
|
||||
struct track *track;
|
||||
unsigned int lib;
|
||||
gchar *path;
|
||||
|
||||
db_for_each(dbe, next, &track_db) {
|
||||
track = TRACK(dbe);
|
||||
|
||||
sscanf(track->tr_path, "%u/%m[^\n]", &lib, &path);
|
||||
if (lib != library_index(track->tr_library)) {
|
||||
track->tr_path = __track_key(track->tr_library, path);
|
||||
db_rekey(&track_db, dbe);
|
||||
} else
|
||||
g_free(path);
|
||||
}
|
||||
}
|
||||
|
||||
const struct database *track_db_get()
|
||||
{
|
||||
return &track_db;
|
||||
}
|
||||
|
||||
unsigned int track_db_count_unplayed()
|
||||
{
|
||||
return unplayed_count;
|
||||
}
|
||||
|
||||
unsigned int track_db_count_plays()
|
||||
{
|
||||
return play_count;
|
||||
}
|
||||
|
||||
unsigned int track_db_average_plays()
|
||||
{
|
||||
if (unplayed_count == track_db.db_size)
|
||||
return 0;
|
||||
return play_count / (track_db.db_size - unplayed_count);
|
||||
}
|
||||
|
||||
struct track *track_alloc_external(const gchar *filepath)
|
||||
{
|
||||
struct track *track = __track_alloc_filepath(filepath);
|
||||
if (track) {
|
||||
track->tr_library = NULL;
|
||||
track->tr_path = g_strdup(filepath);
|
||||
}
|
||||
return track;
|
||||
}
|
||||
|
||||
void track_free_external(struct track *track)
|
||||
{
|
||||
if (TRACK_IS_EXTERNAL(track)) {
|
||||
g_free(track->tr_path);
|
||||
__track_free(track);
|
||||
}
|
||||
}
|
||||
|
||||
struct track *track_add(struct library *library, const gchar *filepath)
|
||||
{
|
||||
unsigned int offset = strlen(library->li_path) + 1;
|
||||
|
@ -219,23 +311,42 @@ struct track *track_get(const unsigned int index)
|
|||
return TRACK(db_at(&track_db, index));
|
||||
}
|
||||
|
||||
struct track *track_lookup(const gchar *filepath)
|
||||
{
|
||||
struct library *library = library_lookup(filepath);
|
||||
unsigned int offset;
|
||||
struct track *track;
|
||||
gchar *key;
|
||||
|
||||
if (!library)
|
||||
return NULL;
|
||||
|
||||
offset = strlen(library->li_path) + 1;
|
||||
key = __track_key(library, g_strdup(filepath + offset));
|
||||
track = TRACK(db_get(&track_db, key));
|
||||
g_free(key);
|
||||
return track;
|
||||
}
|
||||
|
||||
int track_compare(struct track *lhs, struct track *rhs, enum compare_t compare)
|
||||
{
|
||||
switch (compare) {
|
||||
case COMPARE_ARTIST:
|
||||
return artist_compare(lhs->tr_artist, rhs->tr_artist);
|
||||
return artist_compare(lhs->tr_album->al_artist,
|
||||
rhs->tr_album->al_artist);
|
||||
case COMPARE_ALBUM:
|
||||
return album_compare(lhs->tr_album, rhs->tr_album);
|
||||
case COMPARE_COUNT:
|
||||
return lhs->tr_count - rhs->tr_count;
|
||||
case COMPARE_GENRE:
|
||||
return genre_compare(lhs->tr_genre, rhs->tr_genre);
|
||||
return genre_compare(lhs->tr_album->al_genre,
|
||||
rhs->tr_album->al_genre);
|
||||
case COMPARE_LENGTH:
|
||||
return lhs->tr_length - rhs->tr_length;
|
||||
case COMPARE_PLAYED:
|
||||
return date_compare(&lhs->tr_date, &rhs->tr_date);
|
||||
case COMPARE_TITLE:
|
||||
return string_compare(lhs->tr_lower, rhs->tr_lower);
|
||||
return string_compare_tokens(lhs->tr_tokens, rhs->tr_tokens);
|
||||
case COMPARE_TRACK:
|
||||
return lhs->tr_track - rhs->tr_track;
|
||||
case COMPARE_YEAR:
|
||||
|
@ -245,6 +356,12 @@ int track_compare(struct track *lhs, struct track *rhs, enum compare_t compare)
|
|||
return 0; /* We should never get here. */
|
||||
}
|
||||
|
||||
bool track_match_token(struct track *track, const gchar *token)
|
||||
{
|
||||
return string_match_token(token, track->tr_tokens) ||
|
||||
string_match_token(token, track->tr_alts);
|
||||
}
|
||||
|
||||
gchar *track_path(struct track *track)
|
||||
{
|
||||
gchar *path, *res;
|
||||
|
@ -255,12 +372,17 @@ gchar *track_path(struct track *track)
|
|||
g_free(path);
|
||||
return res;
|
||||
}
|
||||
return g_strdup("");
|
||||
return g_strdup(track->tr_path);
|
||||
}
|
||||
|
||||
void track_played(struct track *track)
|
||||
{
|
||||
if (TRACK_IS_EXTERNAL(track))
|
||||
return;
|
||||
if (track->tr_count == 0)
|
||||
unplayed_count--;
|
||||
track->tr_count++;
|
||||
play_count++;
|
||||
date_today(&track->tr_date);
|
||||
track_db_commit();
|
||||
}
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
Import("env")
|
||||
|
||||
env.UsePackage("gtkmm-3.0")
|
||||
env.UsePackage("gstreamer-1.0")
|
||||
|
||||
res = Glob("*.cpp") + SConscript("queue/Sconscript")
|
||||
Return("res")
|
|
@ -0,0 +1,147 @@
|
|||
/*
|
||||
* Copyright 2016 (c) Anna Schumaker.
|
||||
*/
|
||||
#include <core/audio.h>
|
||||
#include <gui/artwork.h>
|
||||
#include <gui/window.h>
|
||||
#include <glib/gi18n.h>
|
||||
|
||||
#define ARTWORK_PREVIEW_SIZE 150
|
||||
|
||||
static struct album *artwork_current = NULL;
|
||||
static unsigned int artwork_timeout = 0;
|
||||
|
||||
static cairo_surface_t *__gui_artwork_scale(cairo_surface_t *orig, int new_h)
|
||||
{
|
||||
int old_h = cairo_image_surface_get_height(orig);
|
||||
int old_w = cairo_image_surface_get_width(orig);
|
||||
int new_w = (old_w * new_h) / old_h;
|
||||
int scale = gtk_widget_get_scale_factor(GTK_WIDGET(gui_artwork()));
|
||||
cairo_content_t content = cairo_surface_get_content(orig);
|
||||
cairo_surface_t *scaled = cairo_surface_create_similar(orig, content,
|
||||
new_w, new_h);
|
||||
cairo_t *cairo = cairo_create(scaled);
|
||||
|
||||
cairo_scale(cairo, (double)(scale * new_w) / old_w,
|
||||
(double)(scale * new_h) / old_h);
|
||||
cairo_set_source_surface(cairo, orig, 0, 0);
|
||||
cairo_paint(cairo);
|
||||
|
||||
cairo_destroy(cairo);
|
||||
return scaled;
|
||||
}
|
||||
|
||||
static cairo_surface_t *__gui_artwork_get(gchar *path, int new_h)
|
||||
{
|
||||
GdkPixbuf *pixbuf = path ? gdk_pixbuf_new_from_file(path, NULL) : NULL;
|
||||
cairo_surface_t *surface = NULL;
|
||||
cairo_surface_t *scaled = NULL;
|
||||
|
||||
if (!pixbuf)
|
||||
return NULL;
|
||||
|
||||
surface = gdk_cairo_surface_create_from_pixbuf(pixbuf, 0, NULL);
|
||||
if (surface) {
|
||||
scaled = __gui_artwork_scale(surface, new_h);
|
||||
cairo_surface_destroy(surface);
|
||||
}
|
||||
|
||||
g_object_unref(G_OBJECT(pixbuf));
|
||||
return scaled;
|
||||
}
|
||||
|
||||
static bool __gui_artwork_set_path(GtkImage *image, gchar *path, int new_h)
|
||||
{
|
||||
cairo_surface_t *surface = __gui_artwork_get(path, new_h);
|
||||
bool status = surface != NULL;
|
||||
|
||||
if (surface) {
|
||||
gtk_image_set_from_surface(image, surface);
|
||||
cairo_surface_destroy(surface);
|
||||
} else
|
||||
gtk_image_set_from_icon_name(image, "image-missing",
|
||||
GTK_ICON_SIZE_DIALOG);
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
static gboolean __gui_artwork_timeout(gpointer data)
|
||||
{
|
||||
struct track *track = audio_cur_track();
|
||||
int height = gui_builder_widget_height("artwork");
|
||||
gchar *path;
|
||||
bool status;
|
||||
|
||||
if (!track || track->tr_album == artwork_current)
|
||||
return G_SOURCE_REMOVE;
|
||||
|
||||
path = album_artwork_path(track->tr_album);
|
||||
status = __gui_artwork_set_path(gui_artwork(), path, height);
|
||||
gtk_widget_set_sensitive(GTK_WIDGET(gui_artwork()), status);
|
||||
artwork_current = status ? track->tr_album : NULL;
|
||||
artwork_timeout = status ? artwork_timeout : 0;
|
||||
|
||||
g_free(path);
|
||||
return status ? G_SOURCE_CONTINUE : G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
void __gui_artwork_update_preview(GtkFileChooser *chooser, gpointer data)
|
||||
{
|
||||
GtkWidget *preview = gtk_file_chooser_get_preview_widget(chooser);
|
||||
gchar *path = gtk_file_chooser_get_preview_filename(chooser);
|
||||
|
||||
__gui_artwork_set_path(GTK_IMAGE(preview), path, ARTWORK_PREVIEW_SIZE);
|
||||
g_free(path);
|
||||
}
|
||||
|
||||
void __gui_artwork_select_cover(GtkButton *button)
|
||||
{
|
||||
struct track *track = audio_cur_track();
|
||||
GtkWidget *preview, *dialog;
|
||||
GtkFileFilter *filter;
|
||||
gchar *path;
|
||||
|
||||
if (!track)
|
||||
return;
|
||||
|
||||
filter = gtk_file_filter_new();
|
||||
preview = gtk_image_new_from_icon_name("", GTK_ICON_SIZE_DIALOG);
|
||||
dialog = gtk_file_chooser_dialog_new("Choose an image", gui_window(),
|
||||
GTK_FILE_CHOOSER_ACTION_OPEN,
|
||||
_("_Cancel"), GTK_RESPONSE_CANCEL,
|
||||
_("_Open"), GTK_RESPONSE_ACCEPT,
|
||||
NULL);
|
||||
|
||||
gtk_file_filter_add_mime_type(filter, "image/*");
|
||||
gtk_file_chooser_set_filter(GTK_FILE_CHOOSER(dialog), filter);
|
||||
gtk_file_chooser_set_show_hidden(GTK_FILE_CHOOSER(dialog), true);
|
||||
gtk_file_chooser_set_preview_widget(GTK_FILE_CHOOSER(dialog), preview);
|
||||
gtk_file_chooser_set_use_preview_label(GTK_FILE_CHOOSER(dialog), false);
|
||||
|
||||
g_signal_connect(dialog, "update-preview",
|
||||
(GCallback)__gui_artwork_update_preview, NULL);
|
||||
|
||||
if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) {
|
||||
path = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));
|
||||
gui_artwork_import(track, path);
|
||||
g_free(path);
|
||||
}
|
||||
|
||||
gtk_widget_destroy(dialog);
|
||||
}
|
||||
|
||||
void gui_artwork_set_cover(void)
|
||||
{
|
||||
if (__gui_artwork_timeout(NULL) != G_SOURCE_CONTINUE)
|
||||
return;
|
||||
artwork_timeout = g_timeout_add(2000, __gui_artwork_timeout, NULL);
|
||||
}
|
||||
|
||||
void gui_artwork_import(struct track *track, gchar *path)
|
||||
{
|
||||
album_artwork_import(track->tr_album, path);
|
||||
if (track == audio_cur_track()) {
|
||||
artwork_current = NULL;
|
||||
gui_artwork_set_cover();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,205 @@
|
|||
/*
|
||||
* Copyright 2014 (c) Anna Schumaker.
|
||||
*/
|
||||
#include <core/string.h>
|
||||
#include <gui/artwork.h>
|
||||
#include <gui/audio.h>
|
||||
#include <gui/idle.h>
|
||||
#include <gui/playlist.h>
|
||||
#include <gui/treeview.h>
|
||||
#include <gui/window.h>
|
||||
|
||||
static guint audio_timeout = 0;
|
||||
static guint popover_timeout = 0;
|
||||
|
||||
static inline void __gui_audio_set_label_markup(GtkLabel *label,
|
||||
const gchar *size,
|
||||
const gchar *text)
|
||||
{
|
||||
const gchar *fmt = "<span size='%s'>%s</span>";
|
||||
gchar *markup = g_markup_printf_escaped(fmt, size, text);
|
||||
gtk_label_set_markup(label, markup);
|
||||
g_free(markup);
|
||||
}
|
||||
|
||||
static void __gui_audio_load(struct track *track)
|
||||
{
|
||||
gchar *duration = string_sec2str(track->tr_length);
|
||||
|
||||
__gui_audio_set_label_markup(gui_title_tag(), "xx-large",
|
||||
track->tr_title);
|
||||
__gui_audio_set_label_markup(gui_album_tag(), "x-large",
|
||||
track->tr_album->al_name);
|
||||
__gui_audio_set_label_markup(gui_artist_tag(), "x-large",
|
||||
track->tr_album->al_artist->ar_name);
|
||||
__gui_audio_set_label_markup(gui_duration(), "large", duration);
|
||||
__gui_audio_set_label_markup(gui_position(), "large", "0:00");
|
||||
gtk_adjustment_set_upper(gui_seek(), track->tr_length);
|
||||
|
||||
gui_pl_system_track_loaded(track);
|
||||
gui_treeview_scroll();
|
||||
gui_artwork_set_cover();
|
||||
gui_idle_enable();
|
||||
g_free(duration);
|
||||
}
|
||||
|
||||
static void __gui_audio_set_pause_text(int n, GstState state)
|
||||
{
|
||||
bool sensitive = true;
|
||||
gchar *text;
|
||||
|
||||
if (n == -1) {
|
||||
sensitive = false;
|
||||
if (state == GST_STATE_PLAYING)
|
||||
text = g_strdup("Keep playing");
|
||||
else
|
||||
text = g_strdup("Paused");
|
||||
} else if (n == 0)
|
||||
text = g_strdup("Pause after this track");
|
||||
else if (n == 1)
|
||||
text = g_strdup("Pause after next track");
|
||||
else
|
||||
text = g_strdup_printf("Pause after %d tracks", n);
|
||||
|
||||
gtk_widget_set_sensitive(GTK_WIDGET(gui_pause_down()), sensitive);
|
||||
gtk_entry_set_text(gui_pause_entry(), text);
|
||||
g_free(text);
|
||||
}
|
||||
|
||||
static void __gui_audio_change_state(GstState state)
|
||||
{
|
||||
bool playing = (state == GST_STATE_PLAYING);
|
||||
gtk_widget_set_visible(GTK_WIDGET(gui_play_button()), !playing);
|
||||
gtk_widget_set_visible(GTK_WIDGET(gui_pause_button()), playing);
|
||||
__gui_audio_set_pause_text(audio_get_pause_count(), state);
|
||||
}
|
||||
|
||||
static void __gui_audio_config_pause(int n)
|
||||
{
|
||||
__gui_audio_set_pause_text(n, audio_cur_state());
|
||||
}
|
||||
|
||||
|
||||
struct audio_callbacks audio_cb = {
|
||||
.audio_cb_load = __gui_audio_load,
|
||||
.audio_cb_state_change = __gui_audio_change_state,
|
||||
.audio_cb_config_pause = __gui_audio_config_pause,
|
||||
};
|
||||
|
||||
|
||||
void __gui_audio_pause(GtkButton *button, gpointer data)
|
||||
{
|
||||
audio_pause();
|
||||
if (audio_get_pause_count() > -1) {
|
||||
gtk_popover_popup(gui_pause_popover());
|
||||
popover_timeout = g_timeout_add_seconds(10,
|
||||
gui_audio_popover_timeout, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
void __gui_audio_pause_change_text(GtkEntry *entry, gpointer data)
|
||||
{
|
||||
const gchar *text = gtk_entry_get_text(entry);
|
||||
int n = audio_get_pause_count();
|
||||
unsigned int i;
|
||||
|
||||
if (g_str_match_string("Keep", text, true))
|
||||
n = -1;
|
||||
else if (g_str_match_string("This", text, true))
|
||||
n = 0;
|
||||
else if (g_str_match_string("Next", text, true))
|
||||
n = 1;
|
||||
else {
|
||||
for (i = 0; text[i] != '\0'; i++) {
|
||||
if (!g_ascii_isdigit(text[i]))
|
||||
continue;
|
||||
if (i > 0 && text[i-1] == '-')
|
||||
i -= 1;
|
||||
n = g_strtod(text + i, NULL);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!audio_pause_after(n))
|
||||
__gui_audio_set_pause_text(audio_get_pause_count(), audio_cur_state());
|
||||
}
|
||||
|
||||
void __gui_audio_pause_inc(GtkButton *button, gpointer data)
|
||||
{
|
||||
audio_pause_after(audio_get_pause_count() + 1);
|
||||
}
|
||||
|
||||
void __gui_audio_pause_dec(GtkButton *button, gpointer data)
|
||||
{
|
||||
audio_pause_after(audio_get_pause_count() - 1);
|
||||
}
|
||||
|
||||
void __gui_audio_pause_popover_popdown(GtkButton *button, gpointer data)
|
||||
{
|
||||
gtk_popover_popdown(gui_pause_popover());
|
||||
#ifdef CONFIG_TESTING
|
||||
gtk_widget_hide(GTK_WIDGET(gui_pause_popover()));
|
||||
#endif /* CONFIG_TESTING */
|
||||
|
||||
g_source_remove(popover_timeout);
|
||||
popover_timeout = 0;
|
||||
}
|
||||
|
||||
void __gui_audio_pause_popover_clear(GtkButton *button, gpointer data)
|
||||
{
|
||||
audio_pause_after(-1);
|
||||
__gui_audio_pause_popover_popdown(button, data);
|
||||
}
|
||||
|
||||
void __gui_audio_seek(GtkRange *range, GtkScrollType type,
|
||||
double value, gpointer data)
|
||||
{
|
||||
audio_seek(value * GST_SECOND);
|
||||
}
|
||||
|
||||
void __gui_audio_volume_changed(GtkScaleButton *button, gdouble value,
|
||||
gpointer data)
|
||||
{
|
||||
audio_set_volume((unsigned int)value);
|
||||
}
|
||||
|
||||
gboolean __gui_audio_can_accel(GtkWidget *widget, guint signal_id)
|
||||
{
|
||||
g_signal_stop_emission_by_name(widget, "can-activate-accel");
|
||||
return !GTK_IS_ENTRY(gtk_window_get_focus(gui_window())) &&
|
||||
gtk_widget_is_visible(widget) &&
|
||||
gtk_widget_is_sensitive(widget);
|
||||
}
|
||||
|
||||
void gui_audio_init()
|
||||
{
|
||||
gtk_scale_button_set_value(gui_volume_button(), audio_get_volume());
|
||||
gtk_button_set_relief(GTK_BUTTON(gui_volume_button()), GTK_RELIEF_NORMAL);
|
||||
|
||||
audio_timeout = g_timeout_add(500, gui_audio_timeout, NULL);
|
||||
}
|
||||
|
||||
void gui_audio_deinit()
|
||||
{
|
||||
g_source_remove(audio_timeout);
|
||||
if (popover_timeout > 0)
|
||||
g_source_remove(popover_timeout);
|
||||
}
|
||||
|
||||
int gui_audio_timeout(gpointer data)
|
||||
{
|
||||
gchar *position = string_sec2str(audio_position() / GST_SECOND);
|
||||
|
||||
gtk_adjustment_set_upper(gui_seek(), audio_duration() / GST_SECOND);
|
||||
gtk_adjustment_set_value(gui_seek(), audio_position() / GST_SECOND);
|
||||
__gui_audio_set_label_markup(gui_position(), "large", position);
|
||||
|
||||
g_free(position);
|
||||
return G_SOURCE_CONTINUE;
|
||||
}
|
||||
|
||||
int gui_audio_popover_timeout(gpointer data)
|
||||
{
|
||||
__gui_audio_pause_popover_popdown(NULL, data);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright 2016 (c) Anna Schumaker.
|
||||
*/
|
||||
#include <gui/builder.h>
|
||||
|
||||
static GtkBuilder *gui_builder = NULL;
|
||||
|
||||
void gui_builder_init(const char *file)
|
||||
{
|
||||
gui_builder = gtk_builder_new_from_file(file);
|
||||
gtk_builder_connect_signals(gui_builder, NULL);
|
||||
}
|
||||
|
||||
void gui_builder_deinit()
|
||||
{
|
||||
g_object_unref(G_OBJECT(gui_builder));
|
||||
gui_builder = NULL;
|
||||
}
|
||||
|
||||
GObject *gui_builder_object(const char *name)
|
||||
{
|
||||
if (gui_builder)
|
||||
return gtk_builder_get_object(gui_builder, name);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
GtkWidget *gui_builder_widget(const char *name)
|
||||
{
|
||||
return GTK_WIDGET(gui_builder_object(name));
|
||||
}
|
||||
|
||||
int gui_builder_widget_height(const char *name)
|
||||
{
|
||||
return gtk_widget_get_allocated_height(gui_builder_widget(name));
|
||||
}
|
||||
|
||||
#ifdef CONFIG_TESTING
|
||||
GtkBuilder *test_get_gui_builder()
|
||||
{
|
||||
return gui_builder;
|
||||
}
|
||||
#endif /* CONFIG_TESTING */
|
|
@ -1,86 +0,0 @@
|
|||
/*
|
||||
* Copyright 2014 (c) Anna Schumaker.
|
||||
*/
|
||||
#include <core/library.h>
|
||||
#include <core/playlist.h>
|
||||
#include <gui/tabs.h>
|
||||
#include <gui/queue/label.h>
|
||||
|
||||
|
||||
class CollectionTab : public Tab {
|
||||
private:
|
||||
CollectionLabel *collection_label;
|
||||
|
||||
public:
|
||||
CollectionTab() : Tab(collection :: get_queue())
|
||||
{
|
||||
tab_builder->add_from_file(gui :: share_file("QueueLabel.ui"));
|
||||
tab_builder->get_widget_derived("CollectionLabel", collection_label);
|
||||
tab_toolbar->init(tab_pq, collection_label, tab_window, T_RANDOM);
|
||||
|
||||
collection_label->init(tab_pq);
|
||||
tab_label = collection_label;
|
||||
|
||||
tab_vbox.pack_start(*tab_window, true, true);
|
||||
gui :: get_widget<Gtk::Notebook>("o_notebook")->insert_page(tab_vbox, *collection_label, 0);
|
||||
}
|
||||
|
||||
~CollectionTab()
|
||||
{
|
||||
tab_unmap();
|
||||
}
|
||||
|
||||
bool on_key_press_event(const std::string &key)
|
||||
{
|
||||
std::vector<unsigned int> ids;
|
||||
|
||||
if (key != "Delete")
|
||||
return Tab :: on_key_press_event(key);
|
||||
|
||||
tab_selected_ids(ids);
|
||||
for (unsigned int i = 0; i < ids.size(); i++)
|
||||
playlist :: add(track_get(ids[i]), "Banned");
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
static CollectionTab *collection_tab;
|
||||
|
||||
static void collection_added(struct queue *queue, unsigned int pos)
|
||||
{
|
||||
if (collection_tab)
|
||||
collection_tab->on_track_added(pos);
|
||||
}
|
||||
|
||||
static void collection_removed(struct queue *queue, unsigned int pos)
|
||||
{
|
||||
if (collection_tab)
|
||||
collection_tab->on_track_removed(pos);
|
||||
}
|
||||
|
||||
static void collection_cleared(struct queue *queue, unsigned int n)
|
||||
{
|
||||
if (collection_tab)
|
||||
collection_tab->on_tracks_cleared(n);
|
||||
}
|
||||
|
||||
static void collection_updated(struct queue *queue, unsigned int pos)
|
||||
{
|
||||
if (collection_tab)
|
||||
collection_tab->on_track_updated(pos);
|
||||
}
|
||||
|
||||
struct queue_ops collection_ops = {
|
||||
collection_added,
|
||||
collection_removed,
|
||||
collection_cleared,
|
||||
collection :: save,
|
||||
collection_updated,
|
||||
};
|
||||
|
||||
void init_collection_tab()
|
||||
{
|
||||
collection_tab = new CollectionTab;
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
/*
|
||||
* Copyright 2016 (c) Anna Schumaker.
|
||||
*/
|
||||
#include <core/audio.h>
|
||||
#include <core/string.h>
|
||||
#include <gui/filter.h>
|
||||
#include <gui/model.h>
|
||||
|
||||
static GtkTreeModelFilter *filter_model = NULL;
|
||||
|
||||
static inline GtkTreePath *__gui_filter_convert_path(GtkTreePath *path)
|
||||
{
|
||||
return gtk_tree_model_filter_convert_path_to_child_path(filter_model,
|
||||
path);
|
||||
}
|
||||
|
||||
static inline gboolean __gui_filter_match_token(struct track *track,
|
||||
const gchar *token,
|
||||
unsigned int how)
|
||||
{
|
||||
switch (how) {
|
||||
case GUI_FILTER_ALBUM:
|
||||
return album_match_token(track->tr_album, token);
|
||||
case GUI_FILTER_ARTIST:
|
||||
return artist_match_token(track->tr_album->al_artist, token);
|
||||
case GUI_FILTER_GENRE:
|
||||
return genre_match_token(track->tr_album->al_genre, token);
|
||||
case GUI_FILTER_TITLE:
|
||||
return track_match_token(track, token);
|
||||
case GUI_FILTER_DEFAULT:
|
||||
return track_match_token(track, token) ||
|
||||
album_match_token(track->tr_album, token) ||
|
||||
artist_match_token(track->tr_album->al_artist, token) ||
|
||||
genre_match_token(track->tr_album->al_genre, token);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static gboolean __gui_filter_visible_func(GtkTreeModel *model,
|
||||
GtkTreeIter *iter,
|
||||
gpointer data)
|
||||
{
|
||||
unsigned int i, how = gtk_combo_box_get_active(gui_filter_how());
|
||||
gchar **search = gui_model_get_playlist()->pl_search;
|
||||
struct track *track;
|
||||
|
||||
if (!search)
|
||||
return TRUE;
|
||||
|
||||
track = gui_model_iter_get_track(iter);
|
||||
for (i = 0; search[i]; i++) {
|
||||
if (!__gui_filter_match_token(track, search[i], how))
|
||||
return FALSE;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
void __gui_filter_search_changed(GtkSearchEntry *search, gpointer data)
|
||||
{
|
||||
playlist_set_search(gui_model_get_playlist(),
|
||||
gtk_entry_get_text(GTK_ENTRY(search)));
|
||||
gtk_tree_model_filter_refilter(gui_filter_get());
|
||||
}
|
||||
|
||||
void __gui_filter_how_changed(int n)
|
||||
{
|
||||
__gui_filter_search_changed(gui_filter_search(), NULL);
|
||||
}
|
||||
|
||||
void gui_filter_init()
|
||||
{
|
||||
GtkTreeModel *model = GTK_TREE_MODEL(gui_model_get());
|
||||
GtkTreeModel *filter = gtk_tree_model_filter_new(model, NULL);
|
||||
filter_model = GTK_TREE_MODEL_FILTER(filter);
|
||||
|
||||
gtk_tree_model_filter_set_visible_func(filter_model,
|
||||
__gui_filter_visible_func,
|
||||
NULL, NULL);
|
||||
}
|
||||
|
||||
void gui_filter_deinit()
|
||||
{
|
||||
g_object_unref(G_OBJECT(filter_model));
|
||||
}
|
||||
|
||||
void gui_filter_set_playlist(struct playlist *playlist)
|
||||
{
|
||||
gchar **search = playlist ? (gchar **)playlist->pl_search : NULL;
|
||||
gchar *text = search ? g_strjoinv(" ", search) : g_strdup("");
|
||||
|
||||
gui_model_set_playlist(playlist);
|
||||
gtk_entry_set_text(GTK_ENTRY(gui_filter_search()), text);
|
||||
g_free(text);
|
||||
}
|
||||
|
||||
GtkTreeModelFilter *gui_filter_get()
|
||||
{
|
||||
return filter_model;
|
||||
}
|
||||
|
||||
struct track *gui_filter_path_get_track(GtkTreePath *path)
|
||||
{
|
||||
GtkTreePath *real = __gui_filter_convert_path(path);
|
||||
struct track *track = real ? gui_model_path_get_track(real) : NULL;
|
||||
gtk_tree_path_free(real);
|
||||
return track;
|
||||
}
|
||||
|
||||
void gui_filter_path_load_track(GtkTreePath *path)
|
||||
{
|
||||
struct track *track = gui_filter_path_get_track(path);
|
||||
unsigned int index = gtk_tree_path_get_indices(path)[0];
|
||||
|
||||
audio_load(track);
|
||||
playlist_current_set(gui_model_get_playlist(), index);
|
||||
}
|
||||
|
||||
unsigned int gui_filter_path_get_index(GtkTreePath *path)
|
||||
{
|
||||
GtkTreePath *real = __gui_filter_convert_path(path);
|
||||
unsigned int ret = gtk_tree_path_get_indices(real)[0];
|
||||
gtk_tree_path_free(real);
|
||||
return ret;
|
||||
}
|
||||
|
||||
GtkTreePath *gui_filter_path_from_index(unsigned int index)
|
||||
{
|
||||
GtkTreePath *real, *path;
|
||||
|
||||
real = gtk_tree_path_new_from_indices(index, -1);
|
||||
path = gtk_tree_model_filter_convert_child_path_to_path(filter_model,
|
||||
real);
|
||||
gtk_tree_path_free(real);
|
||||
return path;
|
||||
}
|
||||
|
||||
void gui_filter_refilter(struct playlist *playlist)
|
||||
{
|
||||
if (!playlist || playlist == gui_model_get_playlist())
|
||||
gtk_tree_model_filter_refilter(gui_filter_get());
|
||||
}
|
271
gui/gst.cpp
271
gui/gst.cpp
|
@ -1,271 +0,0 @@
|
|||
/*
|
||||
* Copyright 2014 (c) Anna Schumaker.
|
||||
*
|
||||
* The gst_init() function parses command line options passed to Ocarina
|
||||
* through argv. Use the command `gst-inspect-1.0 --help-gst` to find
|
||||
* what options are supported.
|
||||
*/
|
||||
#include <core/audio.h>
|
||||
extern "C" {
|
||||
#include <core/string.h>
|
||||
}
|
||||
#include <gui/ocarina.h>
|
||||
#include <gst/gst.h>
|
||||
|
||||
|
||||
static GstBus *gst_bus;
|
||||
static GstElement *gst_player;
|
||||
|
||||
static Gtk::Button *o_next;
|
||||
static Gtk::Button *o_pause;
|
||||
static Gtk::Button *o_play;
|
||||
static Gtk::Button *o_prev;
|
||||
static Gtk::Scale *o_seek;
|
||||
static Gtk::Button *o_stop;
|
||||
|
||||
static Gtk::SpinButton *o_count;
|
||||
static Gtk::CheckButton *o_enabled;
|
||||
|
||||
static Gtk::Label *o_album;
|
||||
static Gtk::Label *o_artist;
|
||||
static Gtk::Label *o_duration;
|
||||
static Gtk::Label *o_position;
|
||||
static Gtk::Label *o_title;
|
||||
static Glib::RefPtr<Gtk::Adjustment> o_progress;
|
||||
|
||||
|
||||
static bool gst_change_state(GstState state)
|
||||
{
|
||||
GstStateChangeReturn ret = gst_element_set_state(gst_player, state);
|
||||
switch (ret) {
|
||||
case GST_STATE_CHANGE_SUCCESS:
|
||||
case GST_STATE_CHANGE_ASYNC:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static void set_markup(Gtk::Label *label, const std::string &size,
|
||||
const std::string &text)
|
||||
{
|
||||
label->set_markup("<span size='" + size + "'>" +
|
||||
Glib::Markup::escape_text(text) + "</span>");
|
||||
}
|
||||
|
||||
|
||||
class GSTDriver : public AudioDriver
|
||||
{
|
||||
public:
|
||||
void load(struct track *track)
|
||||
{
|
||||
gchar *path = track_path(track);
|
||||
gchar *uri = gst_filename_to_uri(path, NULL);
|
||||
gchar *len = string_sec2str(track->tr_length);
|
||||
gchar *str;
|
||||
|
||||
gst_change_state(GST_STATE_NULL);
|
||||
g_object_set(G_OBJECT(gst_player), "uri", uri, NULL);
|
||||
g_free(uri);
|
||||
|
||||
str = g_strdup_printf("From: %s", track->tr_album->al_name);
|
||||
set_markup(o_album, "x-large", str);
|
||||
g_free(str);
|
||||
|
||||
str = g_strdup_printf("By: %s", track->tr_artist->ar_name);
|
||||
set_markup(o_artist, "x-large", str);
|
||||
g_free(str);
|
||||
|
||||
set_markup(o_title, "xx-large", track->tr_title);
|
||||
|
||||
o_duration->set_text(len);
|
||||
g_free(path);
|
||||
g_free(len);
|
||||
|
||||
plist :: track_loaded(track);
|
||||
}
|
||||
|
||||
void play()
|
||||
{
|
||||
if (gst_change_state(GST_STATE_PLAYING)) {
|
||||
o_play->hide();
|
||||
o_pause->show();
|
||||
}
|
||||
}
|
||||
|
||||
void pause()
|
||||
{
|
||||
if (gst_change_state(GST_STATE_PAUSED)) {
|
||||
o_play->show();
|
||||
o_pause->hide();
|
||||
}
|
||||
}
|
||||
|
||||
bool is_playing()
|
||||
{
|
||||
GstState state;
|
||||
gst_element_get_state(gst_player,
|
||||
&state,
|
||||
NULL,
|
||||
GST_CLOCK_TIME_NONE);
|
||||
return state == GST_STATE_PLAYING;
|
||||
}
|
||||
|
||||
void seek_to(int64_t offset)
|
||||
{
|
||||
gst_element_seek_simple(gst_player,
|
||||
GST_FORMAT_TIME,
|
||||
GST_SEEK_FLAG_FLUSH,
|
||||
offset);
|
||||
}
|
||||
|
||||
int64_t position()
|
||||
{
|
||||
int64_t position;
|
||||
if (gst_element_query_position(gst_player,
|
||||
GST_FORMAT_TIME,
|
||||
&position))
|
||||
return position;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int64_t duration()
|
||||
{
|
||||
int64_t duration;
|
||||
if (gst_element_query_duration(gst_player,
|
||||
GST_FORMAT_TIME,
|
||||
&duration))
|
||||
return duration;
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
static GSTDriver *gst_driver;
|
||||
|
||||
|
||||
static void parse_gst_error(GstMessage *error)
|
||||
{
|
||||
GError *err;
|
||||
struct track *track = audio :: current_track();
|
||||
gchar *path;
|
||||
|
||||
gst_message_parse_error(error, &err, NULL);
|
||||
|
||||
if (track) {
|
||||
path = track_path(track);
|
||||
g_print("Error playing file: %s\n", path);
|
||||
g_free(path);
|
||||
}
|
||||
g_print("Error: %s\n", err->message);
|
||||
g_error_free(err);
|
||||
}
|
||||
|
||||
static gboolean on_gst_message(GstBus *bus, GstMessage *message, gpointer data)
|
||||
{
|
||||
switch (GST_MESSAGE_TYPE(message)) {
|
||||
case GST_MESSAGE_ERROR:
|
||||
parse_gst_error(message);
|
||||
audio :: next();
|
||||
audio :: play();
|
||||
break;
|
||||
case GST_MESSAGE_EOS:
|
||||
gst_driver->eos();
|
||||
o_count->set_value(audio :: pause_count());
|
||||
o_enabled->set_active(audio :: pause_enabled());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static bool on_seek(Gtk::ScrollType type, double value)
|
||||
{
|
||||
audio :: seek_to(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void on_pause_count()
|
||||
{
|
||||
o_enabled->set_active(true);
|
||||
audio :: pause_after(true, o_count->get_value());
|
||||
}
|
||||
|
||||
static void on_pause_enabled()
|
||||
{
|
||||
audio :: pause_after(o_enabled->get_active(), o_count->get_value());
|
||||
}
|
||||
|
||||
static bool on_timeout()
|
||||
{
|
||||
gchar *pos = string_sec2str(audio :: position() / GST_SECOND);
|
||||
|
||||
o_progress->set_upper(audio :: duration());
|
||||
o_progress->set_value(audio :: position());
|
||||
o_position->set_text(pos);
|
||||
g_free(pos);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
void gst :: next()
|
||||
{
|
||||
audio :: next();
|
||||
audio :: play();
|
||||
}
|
||||
|
||||
void gst :: toggle()
|
||||
{
|
||||
if (gst_driver->is_playing())
|
||||
audio :: pause();
|
||||
else
|
||||
audio :: play();
|
||||
}
|
||||
|
||||
void gst :: init(int *argc, char ***argv)
|
||||
{
|
||||
gst_init(argc, argv);
|
||||
|
||||
gst_player = gst_element_factory_make("playbin", "ocarina_player");
|
||||
gst_bus = gst_pipeline_get_bus(GST_PIPELINE(gst_player));
|
||||
gst_driver = new GSTDriver();
|
||||
|
||||
o_next = gui :: get_widget<Gtk::Button>("o_next");
|
||||
o_pause = gui :: get_widget<Gtk::Button>("o_pause");
|
||||
o_play = gui :: get_widget<Gtk::Button>("o_play");
|
||||
o_prev = gui :: get_widget<Gtk::Button>("o_prev");
|
||||
o_seek = gui :: get_widget<Gtk::Scale>("o_seek");
|
||||
o_stop = gui :: get_widget<Gtk::Button>("o_stop");
|
||||
|
||||
o_count = gui :: get_widget<Gtk::SpinButton>("o_pause_count");
|
||||
o_enabled = gui :: get_widget<Gtk::CheckButton>("o_pause_enabled");
|
||||
|
||||
o_album = gui :: get_widget<Gtk::Label>("o_album");
|
||||
o_artist = gui :: get_widget<Gtk::Label>("o_artist");
|
||||
o_duration = gui :: get_widget<Gtk::Label>("o_duration");
|
||||
o_position = gui :: get_widget<Gtk::Label>("o_position");
|
||||
o_title = gui :: get_widget<Gtk::Label>("o_title");
|
||||
o_progress = gui :: get_object<Gtk::Adjustment>("o_progress");
|
||||
|
||||
o_next->signal_clicked().connect(sigc::ptr_fun(next));
|
||||
o_pause->signal_clicked().connect(sigc::ptr_fun(audio :: pause));
|
||||
o_play->signal_clicked().connect(sigc::ptr_fun(audio :: play));
|
||||
o_prev->signal_clicked().connect(sigc::ptr_fun(audio :: prev));
|
||||
o_seek->signal_change_value().connect(sigc::ptr_fun(on_seek));
|
||||
o_stop->signal_clicked().connect(sigc::ptr_fun(audio :: stop));
|
||||
|
||||
o_count->signal_changed().connect(sigc::ptr_fun(on_pause_count));
|
||||
o_enabled->signal_toggled().connect(sigc::ptr_fun(on_pause_enabled));
|
||||
|
||||
gst_bus_add_watch(gst_bus, on_gst_message, NULL);
|
||||
Glib :: signal_timeout().connect(sigc::ptr_fun(on_timeout), 500);
|
||||
}
|
||||
|
||||
void gst :: quit()
|
||||
{
|
||||
delete gst_driver;
|
||||
gst_change_state(GST_STATE_NULL);
|
||||
gst_deinit();
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
/*
|
||||
* Copyright 2014 (c) Anna Schumaker.
|
||||
*/
|
||||
#include <core/deck.h>
|
||||
#include <gui/tabs.h>
|
||||
#include <gui/queue/label.h>
|
||||
|
||||
|
||||
class HistoryTab : public Tab {
|
||||
private:
|
||||
HistoryLabel *history_label;
|
||||
|
||||
public:
|
||||
HistoryTab() : Tab(deck :: get_queue())
|
||||
{
|
||||
tab_builder->add_from_file(gui :: share_file("QueueLabel.ui"));
|
||||
tab_builder->get_widget_derived("HistoryLabel", history_label);
|
||||
tab_toolbar->init(tab_pq, history_label, tab_window, 0);
|
||||
|
||||
history_label->init(tab_pq);
|
||||
tab_label = history_label;
|
||||
|
||||
tab_vbox.pack_start(*tab_window, true, true);
|
||||
|
||||
gui :: get_widget<Gtk::Notebook>("o_notebook")->insert_page(tab_vbox, *history_label, 0);
|
||||
}
|
||||
|
||||
~HistoryTab()
|
||||
{
|
||||
tab_unmap();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
static HistoryTab *history_tab;
|
||||
|
||||
static void history_added(struct queue *queue, unsigned int pos)
|
||||
{
|
||||
if (history_tab)
|
||||
history_tab->on_track_added(pos);
|
||||
}
|
||||
|
||||
static void history_removed(struct queue *queue, unsigned int pos)
|
||||
{
|
||||
history_tab->on_track_removed(pos);
|
||||
}
|
||||
|
||||
static void history_cleared(struct queue *queue, unsigned int n)
|
||||
{
|
||||
history_tab->on_tracks_cleared(n);
|
||||
}
|
||||
|
||||
static void history_updated(struct queue *queue, unsigned int pos)
|
||||
{
|
||||
history_tab->on_track_updated(pos);
|
||||
}
|
||||
|
||||
struct queue_ops history_ops = {
|
||||
history_added,
|
||||
history_removed,
|
||||
history_cleared,
|
||||
NULL,
|
||||
history_updated,
|
||||
};
|
||||
|
||||
void init_history_tab()
|
||||
{
|
||||
history_tab = new HistoryTab;
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright 2016 (c) Anna Schumaker.
|
||||
*/
|
||||
#include <gui/idle.h>
|
||||
|
||||
static guint idle_id = 0;
|
||||
|
||||
static gboolean __on_idle(gpointer data)
|
||||
{
|
||||
bool more = idle_run_task();
|
||||
gchar *text = g_strdup_printf("%f%%", idle_progress() * 100);
|
||||
|
||||
gtk_widget_set_visible(GTK_WIDGET(gui_progress_bar()), more);
|
||||
gtk_progress_bar_set_fraction(gui_progress_bar(), idle_progress());
|
||||
gtk_widget_set_tooltip_text(GTK_WIDGET(gui_progress_bar()), text);
|
||||
g_free(text);
|
||||
|
||||
idle_id = more ? idle_id : 0;
|
||||
return more ? G_SOURCE_CONTINUE : G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
void gui_idle_enable()
|
||||
{
|
||||
idle_id = g_idle_add(__on_idle, NULL);
|
||||
}
|
||||
|
||||
void gui_idle_disable()
|
||||
{
|
||||
if (idle_id)
|
||||
g_source_remove(idle_id);
|
||||
}
|
202
gui/manager.cpp
202
gui/manager.cpp
|
@ -1,202 +0,0 @@
|
|||
/*
|
||||
* Copyright 2014 (c) Anna Schumaker.
|
||||
*/
|
||||
extern "C" {
|
||||
#include <core/containers/database.h>
|
||||
#include <core/idle.h>
|
||||
}
|
||||
#include <core/containers/index.h>
|
||||
#include <core/library.h>
|
||||
#include <core/playlist.h>
|
||||
#include <gui/ocarina.h>
|
||||
|
||||
|
||||
static Gtk::Button *c_add;
|
||||
static Gtk::Button *c_update;
|
||||
static Gtk::FileChooserWidget *c_chooser;
|
||||
static Gtk::ProgressBar *c_progress;
|
||||
static Gtk::TreeView *c_treeview;
|
||||
static Glib::RefPtr<Gtk::ListStore> c_list;
|
||||
static Glib::RefPtr<Gtk::CellRendererToggle> c_toggle;
|
||||
|
||||
static class CollectionColumns : public Gtk::TreeModelColumnRecord {
|
||||
public:
|
||||
Gtk::TreeModelColumn<unsigned int> c_id;
|
||||
Gtk::TreeModelColumn<bool> c_enabled;
|
||||
Gtk::TreeModelColumn<unsigned int> c_size;
|
||||
Gtk::TreeModelColumn<Glib::ustring> c_path;
|
||||
|
||||
CollectionColumns()
|
||||
{ add(c_id); add(c_enabled); add(c_size); add(c_path); }
|
||||
} c_cols;
|
||||
|
||||
|
||||
|
||||
static void list_path(struct library *lib)
|
||||
{
|
||||
Gtk::TreeModel::Row row;
|
||||
|
||||
if (lib) {
|
||||
row = *(c_list->append());
|
||||
row[c_cols.c_id] = lib->li_dbe.dbe_index;
|
||||
row[c_cols.c_enabled] = lib->li_enabled;
|
||||
row[c_cols.c_size] = lib->li_size;
|
||||
row[c_cols.c_path] = lib->li_path;
|
||||
}
|
||||
}
|
||||
|
||||
static struct library *get_library_and_row(const Gtk::TreePath &path,
|
||||
Gtk::TreeModel::Row &row)
|
||||
{
|
||||
row = *(c_list->get_iter(path));
|
||||
return library_get(row[c_cols.c_id]);
|
||||
}
|
||||
|
||||
static struct library *get_library(const Gtk::TreePath &path)
|
||||
{
|
||||
Gtk::TreeModel::Row row;
|
||||
return get_library_and_row(path, row);
|
||||
}
|
||||
|
||||
static struct library *current_library_and_row(Gtk::TreeModel::Row &row)
|
||||
{
|
||||
Gtk::TreePath path;
|
||||
Gtk::TreeViewColumn *col;
|
||||
|
||||
c_treeview->get_cursor(path, col);
|
||||
if (path.empty())
|
||||
return NULL;
|
||||
return get_library_and_row(path, row);
|
||||
}
|
||||
|
||||
static struct library *current_library()
|
||||
{
|
||||
Gtk::TreeModel::Row row;
|
||||
return current_library_and_row(row);
|
||||
}
|
||||
|
||||
|
||||
|
||||
void update_paths()
|
||||
{
|
||||
struct library *lib;
|
||||
Gtk::TreeModel::Children::iterator it;
|
||||
|
||||
for (it = c_list->children().begin(); it != c_list->children().end(); it++) {
|
||||
lib = library_get((*it)[c_cols.c_id]);
|
||||
if (lib)
|
||||
(*it)[c_cols.c_size] = lib->li_size;
|
||||
}
|
||||
}
|
||||
|
||||
static void remove_banned_tracks()
|
||||
{
|
||||
index_entry *ent = playlist :: get_tracks("Banned");
|
||||
struct set_iter it;
|
||||
|
||||
if (!ent)
|
||||
return;
|
||||
|
||||
set_for_each(&ent->ie_set, &it)
|
||||
queue_remove_all(collection :: get_queue(), track_get(it.it_val));
|
||||
}
|
||||
|
||||
|
||||
|
||||
static bool on_idle()
|
||||
{
|
||||
bool ret = idle_run_task();
|
||||
|
||||
if (ret) {
|
||||
c_progress->show();
|
||||
c_progress->set_fraction(idle_progress());
|
||||
} else
|
||||
c_progress->hide();
|
||||
|
||||
update_paths();
|
||||
return ret;
|
||||
}
|
||||
|
||||
static void idle_enable()
|
||||
{
|
||||
Glib :: signal_idle().connect(sigc::ptr_fun(on_idle));
|
||||
}
|
||||
|
||||
static void on_add()
|
||||
{
|
||||
list_path(collection :: add(c_chooser->get_filename()));
|
||||
idle_enable();
|
||||
}
|
||||
|
||||
static void on_update()
|
||||
{
|
||||
collection :: update_all();
|
||||
idle_enable();
|
||||
}
|
||||
|
||||
static void on_row_activated(const Gtk::TreePath &path, Gtk::TreeViewColumn *col)
|
||||
{
|
||||
collection :: update(get_library(path));
|
||||
idle_enable();
|
||||
}
|
||||
|
||||
static void on_cursor_changed()
|
||||
{
|
||||
struct library *lib = current_library();
|
||||
if (lib && (c_chooser->get_current_folder() != lib->li_path))
|
||||
c_chooser->set_current_folder(lib->li_path);
|
||||
}
|
||||
|
||||
static bool on_key_pressed(GdkEventKey *event)
|
||||
{
|
||||
struct library *lib;
|
||||
Gtk::TreeModel::Row row;
|
||||
std::string key = gdk_keyval_name(event->keyval);
|
||||
|
||||
if (key != "Delete")
|
||||
return false;
|
||||
|
||||
lib = current_library_and_row(row);
|
||||
collection :: remove(lib);
|
||||
c_list->erase(row);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void on_toggled(const Glib::ustring &str)
|
||||
{
|
||||
Gtk::TreeModel::Row row;
|
||||
struct library *lib = get_library_and_row(Gtk::TreePath(str), row);
|
||||
|
||||
collection :: set_enabled(lib, !lib->li_enabled);
|
||||
row[c_cols.c_enabled] = lib->li_enabled;
|
||||
if (lib->li_enabled)
|
||||
remove_banned_tracks();
|
||||
}
|
||||
|
||||
|
||||
|
||||
void manager :: init()
|
||||
{
|
||||
struct db_entry *library, *next;
|
||||
|
||||
c_add = gui :: get_widget<Gtk::Button>("colmgr_add");
|
||||
c_update = gui :: get_widget<Gtk::Button>("colmgr_update");
|
||||
|
||||
c_chooser = gui :: get_widget<Gtk::FileChooserWidget>("colmgr_chooser");
|
||||
c_progress = gui :: get_widget<Gtk::ProgressBar>("o_idle_progress");
|
||||
c_toggle = gui :: get_object<Gtk::CellRendererToggle>("colmgr_toggle");
|
||||
c_list = gui :: get_object<Gtk::ListStore>("colmgr_list");
|
||||
c_treeview = gui :: get_widget<Gtk::TreeView>("colmgr_treeview");
|
||||
|
||||
c_add->signal_clicked().connect(sigc::ptr_fun(on_add));
|
||||
c_update->signal_clicked().connect(sigc::ptr_fun(on_update));
|
||||
|
||||
c_toggle->signal_toggled().connect(sigc::ptr_fun(on_toggled));
|
||||
c_list->set_sort_column(c_cols.c_path, Gtk::SORT_ASCENDING);
|
||||
c_treeview->signal_row_activated().connect(sigc::ptr_fun(on_row_activated));
|
||||
c_treeview->signal_cursor_changed().connect(sigc::ptr_fun(on_cursor_changed));
|
||||
c_treeview->signal_key_press_event().connect(sigc::ptr_fun(on_key_pressed));
|
||||
|
||||
db_for_each(library, next, library_db_get())
|
||||
list_path(LIBRARY(library));
|
||||
}
|
|
@ -0,0 +1,383 @@
|
|||
/*
|
||||
* Copyright 2016 (c) Anna Schumaker.
|
||||
*/
|
||||
#include <core/audio.h>
|
||||
#include <core/string.h>
|
||||
#include <gui/builder.h>
|
||||
#include <gui/model.h>
|
||||
|
||||
static gboolean __gui_model_iter_nth_child(GtkTreeModel *, GtkTreeIter *,
|
||||
GtkTreeIter *, gint);
|
||||
|
||||
static struct playlist *cur_playlist = NULL;
|
||||
static GObjectClass *parent_class = NULL;
|
||||
static GuiModel *gui_model = NULL;
|
||||
static GType gui_model_type = 0;
|
||||
|
||||
static GType gui_model_columns[GUI_MODEL_N_COLUMNS] = {
|
||||
[GUI_MODEL_TRACK_NR] = G_TYPE_UINT,
|
||||
[GUI_MODEL_TITLE] = G_TYPE_STRING,
|
||||
[GUI_MODEL_LENGTH] = G_TYPE_STRING,
|
||||
[GUI_MODEL_ARTIST] = G_TYPE_STRING,
|
||||
[GUI_MODEL_ALBUM] = G_TYPE_STRING,
|
||||
[GUI_MODEL_YEAR] = G_TYPE_UINT,
|
||||
[GUI_MODEL_GENRE] = G_TYPE_STRING,
|
||||
[GUI_MODEL_COUNT] = G_TYPE_UINT,
|
||||
[GUI_MODEL_LAST_PLAY] = G_TYPE_STRING,
|
||||
[GUI_MODEL_FILE_PATH] = G_TYPE_STRING,
|
||||
[GUI_MODEL_FONT] = G_TYPE_STRING,
|
||||
};
|
||||
|
||||
const GtkTargetEntry gui_model_drag_targets[] = {
|
||||
{ GUI_DRAG_DATA, GTK_TARGET_SAME_APP, 0 },
|
||||
};
|
||||
|
||||
const unsigned int gui_model_n_targets = G_N_ELEMENTS(gui_model_drag_targets);
|
||||
|
||||
static GtkTreeModelFlags __gui_model_get_flags(GtkTreeModel *model)
|
||||
{
|
||||
return GTK_TREE_MODEL_LIST_ONLY;
|
||||
}
|
||||
|
||||
static gint __gui_model_get_n_columns(GtkTreeModel *model)
|
||||
{
|
||||
return GUI_MODEL_N_COLUMNS;
|
||||
}
|
||||
|
||||
static GType __gui_model_get_column_type(GtkTreeModel *model, gint index)
|
||||
{
|
||||
g_return_val_if_fail(index >= 0, G_TYPE_INVALID);
|
||||
g_return_val_if_fail(index < GUI_MODEL_N_COLUMNS, G_TYPE_INVALID);
|
||||
return gui_model_columns[index];
|
||||
}
|
||||
|
||||
static gboolean __gui_model_get_iter(GtkTreeModel *model, GtkTreeIter *iter,
|
||||
GtkTreePath *path)
|
||||
{
|
||||
gint *indices, depth;
|
||||
|
||||
g_assert(path != NULL);
|
||||
indices = gtk_tree_path_get_indices_with_depth(path, &depth);
|
||||
g_assert(depth == 1);
|
||||
|
||||
return __gui_model_iter_nth_child(model, iter, NULL, indices[0]);
|
||||
}
|
||||
|
||||
static GtkTreePath *__gui_model_get_path(GtkTreeModel *model, GtkTreeIter *iter)
|
||||
{
|
||||
GtkTreePath *path;
|
||||
unsigned int pos;
|
||||
|
||||
g_return_val_if_fail(iter != NULL, FALSE);
|
||||
g_return_val_if_fail(iter->user_data, FALSE);
|
||||
|
||||
path = gtk_tree_path_new();
|
||||
pos = playlist_iter_index(cur_playlist, iter->user_data);
|
||||
gtk_tree_path_append_index(path, pos);
|
||||
return path;
|
||||
}
|
||||
|
||||
static void __gui_model_get_value(GtkTreeModel *model, GtkTreeIter *iter,
|
||||
gint column, GValue *value)
|
||||
{
|
||||
struct track *track = playlist_iter_track(iter->user_data);
|
||||
gchar *str;
|
||||
|
||||
g_return_if_fail(iter != NULL);
|
||||
g_return_if_fail(iter->user_data != NULL);
|
||||
g_return_if_fail(column < GUI_MODEL_N_COLUMNS);
|
||||
|
||||
g_value_init(value, gui_model_columns[column]);
|
||||
|
||||
switch (column) {
|
||||
case GUI_MODEL_TRACK_NR:
|
||||
g_value_set_uint(value, track->tr_track);
|
||||
break;
|
||||
case GUI_MODEL_TITLE:
|
||||
g_value_set_static_string(value, track->tr_title);
|
||||
break;
|
||||
case GUI_MODEL_LENGTH:
|
||||
g_value_take_string(value, string_sec2str(track->tr_length));
|
||||
break;
|
||||
case GUI_MODEL_ARTIST:
|
||||
g_value_set_static_string(value, track->tr_album->al_artist->ar_name);
|
||||
break;
|
||||
case GUI_MODEL_ALBUM:
|
||||
g_value_set_static_string(value, track->tr_album->al_name);
|
||||
break;
|
||||
case GUI_MODEL_YEAR:
|
||||
g_value_set_uint(value, track->tr_album->al_year);
|
||||
break;
|
||||
case GUI_MODEL_GENRE:
|
||||
g_value_set_static_string(value, track->tr_album->al_genre->ge_name);
|
||||
break;
|
||||
case GUI_MODEL_COUNT:
|
||||
g_value_set_uint(value, track->tr_count);
|
||||
break;
|
||||
case GUI_MODEL_LAST_PLAY:
|
||||
g_value_take_string(value, track_last_play(track));
|
||||
break;
|
||||
case GUI_MODEL_FILE_PATH:
|
||||
str = track_path(track);
|
||||
g_value_take_string(value, g_markup_escape_text(str, -1));
|
||||
g_free(str);
|
||||
break;
|
||||
case GUI_MODEL_FONT:
|
||||
str = (track == audio_cur_track()) ? "bold" : "";
|
||||
g_value_take_string(value, g_strdup(str));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static gboolean __gui_model_iter_next(GtkTreeModel *model, GtkTreeIter *iter)
|
||||
{
|
||||
g_return_val_if_fail(iter != NULL, FALSE);
|
||||
g_return_val_if_fail(iter->user_data, FALSE);
|
||||
|
||||
iter->user_data = playlist_iter_next(iter->user_data);
|
||||
return iter->user_data != NULL;
|
||||
}
|
||||
|
||||
static gboolean __gui_model_iter_children(GtkTreeModel *model, GtkTreeIter *iter,
|
||||
GtkTreeIter *parent)
|
||||
{
|
||||
return __gui_model_iter_nth_child(model, iter, parent, 0);
|
||||
}
|
||||
|
||||
static gboolean __gui_model_iter_has_child(GtkTreeModel *model, GtkTreeIter *iter)
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static gint __gui_model_iter_n_children(GtkTreeModel *model, GtkTreeIter *iter)
|
||||
{
|
||||
if (iter != NULL || !cur_playlist)
|
||||
return 0;
|
||||
return playlist_size(cur_playlist);
|
||||
}
|
||||
|
||||
static gboolean __gui_model_iter_nth_child(GtkTreeModel *model,
|
||||
GtkTreeIter *iter,
|
||||
GtkTreeIter *parent,
|
||||
gint n)
|
||||
{
|
||||
if (parent || !cur_playlist || n >= playlist_size(cur_playlist))
|
||||
return FALSE;
|
||||
|
||||
iter->stamp = gui_model->gm_stamp;
|
||||
iter->user_data = playlist_iter_get(cur_playlist, n);
|
||||
return iter->user_data != NULL;
|
||||
}
|
||||
|
||||
static gboolean __gui_model_iter_parent(GtkTreeModel *model, GtkTreeIter *iter,
|
||||
GtkTreeIter *child)
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static gboolean __gui_model_drag_data_get(GtkTreeDragSource *drag_source,
|
||||
GtkTreePath *path,
|
||||
GtkSelectionData *selection_data)
|
||||
{
|
||||
struct gui_model_drag_data *data = g_malloc(sizeof(*data));
|
||||
|
||||
data->drag_row = gtk_tree_path_get_indices(path)[0];
|
||||
data->drag_track = gui_model_path_get_track(path);
|
||||
|
||||
gtk_selection_data_set(selection_data, gdk_atom_intern(GUI_DRAG_DATA, false),
|
||||
8 /* bytes */, (void *)data, sizeof(*data));
|
||||
g_free(data);
|
||||
return true;
|
||||
}
|
||||
|
||||
static gboolean __gui_model_drag_data_delete(GtkTreeDragSource *drag_source,
|
||||
GtkTreePath *path)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
static void __gui_model_init(GuiModel *model)
|
||||
{
|
||||
model->gm_stamp = g_random_int();
|
||||
}
|
||||
|
||||
static void __gui_model_finalize(GObject *object)
|
||||
{
|
||||
parent_class->finalize(object);
|
||||
}
|
||||
|
||||
static void __gui_model_class_init(GuiModelClass *class)
|
||||
{
|
||||
GObjectClass *object_class = (GObjectClass *)class;
|
||||
|
||||
object_class->finalize = __gui_model_finalize;
|
||||
parent_class = g_type_class_peek_parent(class);
|
||||
}
|
||||
|
||||
static void __gui_tree_model_init(GtkTreeModelIface *iface)
|
||||
{
|
||||
iface->get_flags = __gui_model_get_flags;
|
||||
iface->get_n_columns = __gui_model_get_n_columns;
|
||||
iface->get_column_type = __gui_model_get_column_type;
|
||||
iface->get_iter = __gui_model_get_iter;
|
||||
iface->get_path = __gui_model_get_path;
|
||||
iface->get_value = __gui_model_get_value;
|
||||
iface->iter_next = __gui_model_iter_next;
|
||||
iface->iter_children = __gui_model_iter_children;
|
||||
iface->iter_has_child = __gui_model_iter_has_child;
|
||||
iface->iter_n_children = __gui_model_iter_n_children;
|
||||
iface->iter_nth_child = __gui_model_iter_nth_child;
|
||||
iface->iter_parent = __gui_model_iter_parent;
|
||||
}
|
||||
|
||||
static void __gui_drag_source_init(GtkTreeDragSourceIface *iface)
|
||||
{
|
||||
iface->drag_data_get = __gui_model_drag_data_get;
|
||||
iface->drag_data_delete = __gui_model_drag_data_delete;
|
||||
}
|
||||
|
||||
static const GTypeInfo gui_model_type_info = {
|
||||
.class_size = sizeof(GuiModelClass),
|
||||
.base_init = NULL,
|
||||
.base_finalize = NULL,
|
||||
.class_init = (GClassInitFunc)__gui_model_class_init,
|
||||
.class_finalize = NULL,
|
||||
.class_data = NULL,
|
||||
.instance_size = sizeof(GuiModel),
|
||||
.n_preallocs = 0,
|
||||
.instance_init = (GInstanceInitFunc)__gui_model_init,
|
||||
};
|
||||
|
||||
static const GInterfaceInfo gui_tree_model = {
|
||||
.interface_init = (GInterfaceInitFunc)__gui_tree_model_init,
|
||||
.interface_finalize = NULL,
|
||||
.interface_data = NULL,
|
||||
};
|
||||
|
||||
static const GInterfaceInfo gui_drag_source = {
|
||||
.interface_init = (GInterfaceInitFunc)__gui_drag_source_init,
|
||||
.interface_finalize = NULL,
|
||||
.interface_data = NULL,
|
||||
};
|
||||
|
||||
|
||||
void gui_model_init(void)
|
||||
{
|
||||
gui_model_type = g_type_register_static(G_TYPE_OBJECT, "GuiModel",
|
||||
&gui_model_type_info,
|
||||
(GTypeFlags)0);
|
||||
g_type_add_interface_static(gui_model_type, GTK_TYPE_TREE_MODEL,
|
||||
&gui_tree_model);
|
||||
g_type_add_interface_static(gui_model_type, GTK_TYPE_TREE_DRAG_SOURCE,
|
||||
&gui_drag_source);
|
||||
|
||||
|
||||
gui_model = g_object_new(gui_model_type, NULL);
|
||||
g_assert(gui_model != NULL);
|
||||
}
|
||||
|
||||
void gui_model_deinit(void)
|
||||
{
|
||||
g_object_unref(gui_model);
|
||||
gui_model_type = 0;
|
||||
gui_model = NULL;
|
||||
cur_playlist = NULL;
|
||||
}
|
||||
|
||||
static void __gui_model_set_runtime(void)
|
||||
{
|
||||
gchar *len = NULL;
|
||||
|
||||
if (cur_playlist)
|
||||
len = string_sec2str_long(cur_playlist->pl_length);
|
||||
|
||||
gtk_label_set_text(gui_model_runtime(), len);
|
||||
g_free(len);
|
||||
}
|
||||
|
||||
static gboolean __gui_model_foreach_changed(GtkTreeModel *model, GtkTreePath *path,
|
||||
GtkTreeIter *iter, gpointer data)
|
||||
{
|
||||
if (!data || data == gui_model_iter_get_track(iter))
|
||||
gtk_tree_model_row_changed(model, path, iter);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
GuiModel *gui_model_get(void)
|
||||
{
|
||||
return gui_model;
|
||||
}
|
||||
|
||||
GType gui_model_get_type()
|
||||
{
|
||||
return gui_model_type;
|
||||
}
|
||||
|
||||
void gui_model_add(struct playlist *playlist, struct track *track)
|
||||
{
|
||||
GtkTreePath *path;
|
||||
GtkTreeIter iter;
|
||||
|
||||
if (cur_playlist != playlist)
|
||||
return;
|
||||
|
||||
path = gtk_tree_path_new_from_indices(0, -1);
|
||||
__gui_model_get_iter(GTK_TREE_MODEL(gui_model), &iter, path);
|
||||
gtk_tree_model_row_inserted(GTK_TREE_MODEL(gui_model), path, &iter);
|
||||
gtk_tree_path_free(path);
|
||||
|
||||
__gui_model_set_runtime();
|
||||
}
|
||||
|
||||
void gui_model_remove(struct playlist *playlist, struct track *track,
|
||||
unsigned int n)
|
||||
{
|
||||
GtkTreePath *path;
|
||||
unsigned int i;
|
||||
|
||||
if (cur_playlist != playlist)
|
||||
return;
|
||||
|
||||
path = gtk_tree_path_new_from_indices(n - 1, -1);
|
||||
for (i = 0; i < n; i++) {
|
||||
gtk_tree_model_row_deleted(GTK_TREE_MODEL(gui_model), path);
|
||||
gtk_tree_path_prev(path);
|
||||
}
|
||||
gtk_tree_path_free(path);
|
||||
|
||||
__gui_model_set_runtime();
|
||||
}
|
||||
|
||||
void gui_model_update(struct playlist *playlist, struct track *track)
|
||||
{
|
||||
if (cur_playlist == playlist)
|
||||
gtk_tree_model_foreach(GTK_TREE_MODEL(gui_model),
|
||||
__gui_model_foreach_changed, track);
|
||||
|
||||
__gui_model_set_runtime();
|
||||
}
|
||||
|
||||
void gui_model_set_playlist(struct playlist *playlist)
|
||||
{
|
||||
if (cur_playlist)
|
||||
gui_model_remove(cur_playlist, NULL, playlist_size(cur_playlist));
|
||||
|
||||
cur_playlist = playlist;
|
||||
__gui_model_set_runtime();
|
||||
|
||||
if (playlist && playlist_size(playlist) > 0)
|
||||
gui_model_add(playlist, 0);
|
||||
}
|
||||
|
||||
struct playlist *gui_model_get_playlist(void)
|
||||
{
|
||||
return cur_playlist;
|
||||
}
|
||||
|
||||
struct track * gui_model_path_get_track(GtkTreePath *path)
|
||||
{
|
||||
GtkTreeIter iter;
|
||||
|
||||
__gui_model_get_iter(GTK_TREE_MODEL(gui_model), &iter, path);
|
||||
return gui_model_iter_get_track(&iter);
|
||||
}
|
|
@ -0,0 +1,151 @@
|
|||
/*
|
||||
* Copyright 2014 (c) Anna Schumaker.
|
||||
*/
|
||||
#include <core/core.h>
|
||||
#include <gui/audio.h>
|
||||
#include <gui/builder.h>
|
||||
#include <gui/filter.h>
|
||||
#include <gui/idle.h>
|
||||
#include <gui/model.h>
|
||||
#include <gui/playlist.h>
|
||||
#include <gui/sidebar.h>
|
||||
#include <gui/treeview.h>
|
||||
#include <gui/window.h>
|
||||
|
||||
#define OCARINA_FLAGS (G_APPLICATION_HANDLES_COMMAND_LINE)
|
||||
|
||||
static const GOptionEntry ocarina_options[] = {
|
||||
{ "next", 'n', 0, G_OPTION_ARG_NONE, NULL, "Play next track", NULL },
|
||||
{ "pause", 'P', 0, G_OPTION_ARG_NONE, NULL, "Pause playback", NULL },
|
||||
{ "play", 'p', 0, G_OPTION_ARG_NONE, NULL, "Start playback", NULL },
|
||||
{ "previous", 'N', 0, G_OPTION_ARG_NONE, NULL, "Play previous track", NULL },
|
||||
{ "sync", 's', 0, G_OPTION_ARG_NONE, NULL, "Don't run background tasks", NULL },
|
||||
{ "toggle", 't', 0, G_OPTION_ARG_NONE, NULL, "Toggle playback state", NULL },
|
||||
{ "version", 'v', 0, G_OPTION_ARG_NONE, NULL, "Print version and exit", NULL },
|
||||
{ NULL },
|
||||
};
|
||||
|
||||
#ifndef CONFIG_DEBUG
|
||||
const static gchar *OCARINA_APP = "org.gtk.ocarina";
|
||||
#else
|
||||
const static gchar *OCARINA_APP = "org.gtk.ocarina-debug";
|
||||
#endif
|
||||
|
||||
static enum idle_sync_t idle_sync = IDLE_ASYNC;
|
||||
static int startup_argc;
|
||||
static char **startup_argv;
|
||||
|
||||
static gchar *find_file_path(const gchar *file)
|
||||
{
|
||||
gchar *path = g_strjoin("/", "share", "ocarina", file, NULL);
|
||||
|
||||
if (g_file_test(path, G_FILE_TEST_IS_REGULAR))
|
||||
return path;
|
||||
g_free(path);
|
||||
|
||||
return g_strjoin("/", "/usr", "share", "ocarina", file, NULL);
|
||||
}
|
||||
|
||||
static void __ocarina_activate(GApplication *application, gpointer data)
|
||||
{
|
||||
gtk_application_add_window(GTK_APPLICATION(application), gui_window());
|
||||
}
|
||||
|
||||
static int __ocarina_local_options(GApplication *application,
|
||||
GVariantDict *options, gpointer data)
|
||||
{
|
||||
if (g_variant_dict_contains(options, "sync"))
|
||||
idle_sync = IDLE_SYNC;
|
||||
if (!g_variant_dict_contains(options, "version"))
|
||||
return -1;
|
||||
g_printf("Ocarina %s\n", get_version());
|
||||
g_printf("GTK+ %u.%u.%u\n", GTK_MAJOR_VERSION, GTK_MINOR_VERSION, GTK_MICRO_VERSION);
|
||||
g_printf("%s\n", gst_version_string());
|
||||
g_printf("%s, %s\n", __DATE__, __TIME__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int __ocarina_command_line(GApplication *application,
|
||||
GApplicationCommandLine *command,
|
||||
gpointer data)
|
||||
{
|
||||
GVariantDict *options;
|
||||
gchar **args;
|
||||
|
||||
g_application_activate(application);
|
||||
|
||||
args = g_application_command_line_get_arguments(command, NULL);
|
||||
if (args && args[1]) {
|
||||
audio_load_filepath(args[1]);
|
||||
g_strfreev(args);
|
||||
}
|
||||
|
||||
options = g_application_command_line_get_options_dict(command);
|
||||
if (g_variant_dict_contains(options, "next"))
|
||||
audio_next();
|
||||
else if (g_variant_dict_contains(options, "pause"))
|
||||
audio_pause();
|
||||
else if (g_variant_dict_contains(options, "play"))
|
||||
audio_play();
|
||||
else if (g_variant_dict_contains(options, "previous"))
|
||||
audio_prev();
|
||||
else if (g_variant_dict_contains(options, "toggle")) {
|
||||
if (audio_cur_state() == GST_STATE_PLAYING)
|
||||
audio_pause();
|
||||
else
|
||||
audio_play();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void __ocarina_startup(GApplication *application, gpointer data)
|
||||
{
|
||||
gchar *ui = find_file_path("ocarina.ui");
|
||||
gchar *icon = find_file_path("ocarina.png");
|
||||
|
||||
gui_builder_init(ui);
|
||||
core_init(&startup_argc, &startup_argv, &playlist_cb, &audio_cb, idle_sync);
|
||||
gui_window_init(icon);
|
||||
gui_model_init();
|
||||
gui_filter_init();
|
||||
gui_treeview_init();
|
||||
gui_sidebar_init();
|
||||
gui_playlist_init();
|
||||
gui_audio_init();
|
||||
|
||||
gui_idle_enable();
|
||||
|
||||
g_free(ui);
|
||||
g_free(icon);
|
||||
}
|
||||
|
||||
static void __ocarina_shutdown(GApplication *application, gpointer data)
|
||||
{
|
||||
gui_idle_disable();
|
||||
gui_audio_deinit();
|
||||
core_deinit();
|
||||
|
||||
gui_treeview_deinit();
|
||||
gui_filter_deinit();
|
||||
gui_model_deinit();
|
||||
gui_window_deinit();
|
||||
gui_builder_deinit();
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
GtkApplication *ocarina = gtk_application_new(OCARINA_APP, OCARINA_FLAGS);
|
||||
startup_argc = argc;
|
||||
startup_argv = argv;
|
||||
|
||||
g_application_add_main_option_entries(G_APPLICATION(ocarina), ocarina_options);
|
||||
g_application_add_option_group(G_APPLICATION(ocarina), gst_init_get_option_group());
|
||||
|
||||
g_signal_connect(G_APPLICATION(ocarina), "activate", (GCallback)__ocarina_activate, NULL);
|
||||
g_signal_connect(G_APPLICATION(ocarina), "handle-local-options",
|
||||
(GCallback)__ocarina_local_options, NULL);
|
||||
g_signal_connect(G_APPLICATION(ocarina), "command-line", (GCallback)__ocarina_command_line, NULL);
|
||||
g_signal_connect(G_APPLICATION(ocarina), "startup", (GCallback)__ocarina_startup, NULL);
|
||||
g_signal_connect(G_APPLICATION(ocarina), "shutdown", (GCallback)__ocarina_shutdown, NULL);
|
||||
return g_application_run(G_APPLICATION(ocarina), argc, argv);
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
/*
|
||||
* Copyright 2014 (c) Anna Schumaker.
|
||||
*/
|
||||
#include <core/core.h>
|
||||
#include <gui/ocarina.h>
|
||||
#include <gui/tabs.h>
|
||||
|
||||
|
||||
static std::string ocarina_dir = "";
|
||||
static Glib::RefPtr<Gtk::Application> ocarina_app;
|
||||
|
||||
|
||||
struct core_init_data init_data = {
|
||||
&collection_ops,
|
||||
&history_ops,
|
||||
&playlist_ops,
|
||||
&tempq_ops,
|
||||
};
|
||||
|
||||
namespace gui
|
||||
{
|
||||
Glib::RefPtr<Gtk::Builder> __O_BUILDER;
|
||||
|
||||
const std::string share_file(const std::string &f)
|
||||
{
|
||||
return ocarina_dir + f;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void setup_share(const std::string &path)
|
||||
{
|
||||
char buf[1024];
|
||||
ssize_t len = readlink("/proc/self/exe", buf, sizeof(buf) - 1);
|
||||
|
||||
if (len == -1)
|
||||
return;
|
||||
buf[len] = '\0';
|
||||
|
||||
ocarina_dir = std::string(buf);
|
||||
ocarina_dir = ocarina_dir.substr(0, ocarina_dir.size() - 11);
|
||||
ocarina_dir = ocarina_dir + "share/ocarina/";
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
Gtk::Window *window;
|
||||
|
||||
setup_share(argv[0]);
|
||||
ocarina_app = Gtk::Application::create(argc, argv, "org.gtkmm.ocarina");
|
||||
|
||||
gui :: __O_BUILDER = Gtk::Builder::create();
|
||||
if (!gui::__O_BUILDER->add_from_file(gui::share_file("ocarina6.glade")))
|
||||
exit(1);
|
||||
|
||||
gst :: init(&argc, &argv);
|
||||
|
||||
core :: init(&init_data);
|
||||
|
||||
plist :: init();
|
||||
manager :: init();
|
||||
init_tabs();
|
||||
window = window_init();
|
||||
post_init_tabs();
|
||||
|
||||
ocarina_app->run(*window);
|
||||
cleanup_tabs();
|
||||
gst :: quit();
|
||||
core :: deinit();
|
||||
|
||||
return 0;
|
||||
}
|
|
@ -0,0 +1,259 @@
|
|||
/*
|
||||
* Copyright 2016 (c) Anna Schumaker.
|
||||
*/
|
||||
#include <core/idle.h>
|
||||
#include <core/string.h>
|
||||
#include <gui/filter.h>
|
||||
#include <gui/model.h>
|
||||
#include <gui/playlist.h>
|
||||
#include <gui/treeview.h>
|
||||
#include <gui/sidebar.h>
|
||||
|
||||
static void (*update_size[PL_MAX_TYPE])(struct playlist *) = {
|
||||
[PL_SYSTEM] = gui_pl_system_update,
|
||||
[PL_ARTIST] = gui_pl_artist_update,
|
||||
[PL_LIBRARY] = gui_pl_library_update,
|
||||
[PL_USER] = gui_pl_user_update,
|
||||
};
|
||||
|
||||
static void (*select_playlist[PL_MAX_TYPE])(struct playlist *) = {
|
||||
[PL_SYSTEM] = gui_pl_system_select,
|
||||
[PL_ARTIST] = gui_pl_artist_select,
|
||||
[PL_LIBRARY] = gui_pl_library_select,
|
||||
[PL_USER] = gui_pl_user_select,
|
||||
};
|
||||
|
||||
static inline void __gui_playlist_update_size(struct playlist *playlist)
|
||||
{
|
||||
update_size[playlist->pl_type](playlist);
|
||||
}
|
||||
|
||||
static void __gui_playlist_alloc(struct playlist *playlist)
|
||||
{
|
||||
if (playlist->pl_type == PL_ARTIST)
|
||||
gui_pl_artist_add(playlist);
|
||||
}
|
||||
|
||||
static void __gui_playlist_added(struct playlist *playlist, struct track *track)
|
||||
{
|
||||
gui_model_add(playlist, track);
|
||||
gui_filter_refilter(playlist);
|
||||
__gui_playlist_update_size(playlist);
|
||||
}
|
||||
|
||||
static void __gui_playlist_removed(struct playlist *playlist, struct track *track,
|
||||
unsigned int n)
|
||||
{
|
||||
gui_model_remove(playlist, track, n);
|
||||
__gui_playlist_update_size(playlist);
|
||||
}
|
||||
|
||||
|
||||
struct playlist_callbacks playlist_cb = {
|
||||
.pl_cb_alloc = __gui_playlist_alloc,
|
||||
.pl_cb_added = __gui_playlist_added,
|
||||
.pl_cb_removed = __gui_playlist_removed,
|
||||
.pl_cb_updated = gui_model_update,
|
||||
};
|
||||
|
||||
|
||||
static void __gui_playlist_add_selected_to(struct playlist *playlist)
|
||||
{
|
||||
GList *cur, *list = NULL;
|
||||
struct track *track;
|
||||
|
||||
if (!playlist)
|
||||
return;
|
||||
|
||||
list = gui_treeview_list_selected_tracks();
|
||||
cur = g_list_first(list);
|
||||
while (cur) {
|
||||
track = (struct track *)cur->data;
|
||||
playlist_add(playlist, track);
|
||||
cur = g_list_next(cur);
|
||||
}
|
||||
g_list_free(list);
|
||||
}
|
||||
|
||||
void __gui_playlist_add_favorites(GtkMenuItem *item, gpointer data)
|
||||
{
|
||||
__gui_playlist_add_selected_to(playlist_lookup(PL_SYSTEM, "Favorites"));
|
||||
}
|
||||
|
||||
void __gui_playlist_add_hidden(GtkMenuItem *item, gpointer data)
|
||||
{
|
||||
__gui_playlist_add_selected_to(playlist_lookup(PL_SYSTEM, "Hidden"));
|
||||
}
|
||||
|
||||
void __gui_playlist_add_user(GtkMenuItem *item, gpointer data)
|
||||
{
|
||||
__gui_playlist_add_selected_to(gui_pl_user_add_dialog());
|
||||
}
|
||||
|
||||
void __gui_playlist_add_other(GtkMenuItem *item, gpointer data)
|
||||
{
|
||||
__gui_playlist_add_selected_to(data);
|
||||
}
|
||||
|
||||
void __gui_playlist_add_queued(GtkMenuItem *item, gpointer data)
|
||||
{
|
||||
__gui_playlist_add_selected_to(playlist_lookup(PL_SYSTEM, "Queued Tracks"));
|
||||
}
|
||||
|
||||
void __gui_playlist_delete(GtkMenuItem *item, gpointer data)
|
||||
{
|
||||
struct playlist *playlist = gui_model_get_playlist();
|
||||
GList *cur, *list = NULL;
|
||||
struct track *track;
|
||||
|
||||
if (!playlist)
|
||||
return;
|
||||
|
||||
list = gui_treeview_list_selected_tracks();
|
||||
cur = g_list_first(list);
|
||||
while (cur) {
|
||||
track = (struct track *)cur->data;
|
||||
playlist_remove(playlist, track);
|
||||
cur = g_list_next(cur);
|
||||
}
|
||||
g_list_free(list);
|
||||
}
|
||||
|
||||
void __gui_playlist_keypress(GtkTreeView *treeview, GdkEventKey *event,
|
||||
gpointer data)
|
||||
{
|
||||
switch (event->keyval) {
|
||||
case GDK_KEY_f:
|
||||
__gui_playlist_add_favorites(NULL, NULL);
|
||||
break;
|
||||
case GDK_KEY_h:
|
||||
__gui_playlist_add_hidden(NULL, NULL);
|
||||
break;
|
||||
case GDK_KEY_p:
|
||||
__gui_playlist_add_user(NULL, NULL);
|
||||
break;
|
||||
case GDK_KEY_q:
|
||||
__gui_playlist_add_queued(NULL, NULL);
|
||||
break;
|
||||
case GDK_KEY_Delete:
|
||||
__gui_playlist_delete(NULL, NULL);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static GtkWidget *__gui_playlist_build_submenu(void)
|
||||
{
|
||||
struct playlist *playlist;
|
||||
GList *list = gui_pl_user_list();
|
||||
GList *cur = g_list_first(list);
|
||||
GtkWidget *submenu, *item;
|
||||
|
||||
if (!list)
|
||||
return NULL;
|
||||
|
||||
submenu = gtk_menu_new();
|
||||
while (cur) {
|
||||
playlist = (struct playlist *)cur->data;
|
||||
item = gtk_menu_item_new_with_label(playlist->pl_name);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(submenu), item);
|
||||
g_signal_connect(item, "activate",
|
||||
G_CALLBACK(__gui_playlist_add_other),
|
||||
playlist);
|
||||
cur = g_list_next(cur);
|
||||
}
|
||||
|
||||
gtk_widget_show_all(submenu);
|
||||
g_list_free(list);
|
||||
return submenu;
|
||||
}
|
||||
|
||||
bool __gui_playlist_button_press(GtkTreeView *treeview, GdkEventButton *event,
|
||||
gpointer data)
|
||||
{
|
||||
GtkWidget *submenu;
|
||||
|
||||
if (event->button != GDK_BUTTON_SECONDARY)
|
||||
return false;
|
||||
|
||||
submenu = __gui_playlist_build_submenu();
|
||||
gtk_menu_item_set_submenu(gui_rc_add_to_other(), submenu);
|
||||
gtk_widget_set_visible(GTK_WIDGET(gui_rc_add_to_other()),
|
||||
submenu != NULL);
|
||||
|
||||
gui_treeview_select_path_at_pos(event->x, event->y);
|
||||
gtk_menu_popup_at_pointer(gui_rc_menu(), (GdkEvent *)event);
|
||||
return true;
|
||||
}
|
||||
|
||||
void __gui_playlist_row_activated(GtkTreeView *treeview, GtkTreePath *path,
|
||||
GtkTreeViewColumn *col, gpointer data)
|
||||
{
|
||||
struct playlist *prev = playlist_current();
|
||||
|
||||
gui_sidebar_filter_path_select(path);
|
||||
__gui_playlist_update_size(prev);
|
||||
__gui_playlist_update_size(playlist_current());
|
||||
}
|
||||
|
||||
void __gui_playlist_row_collapsed(GtkTreeView *treeview, GtkTreeIter *iter,
|
||||
GtkTreePath *path, gpointer data)
|
||||
{
|
||||
gui_sidebar_filter_row_expanded(iter, false);
|
||||
}
|
||||
|
||||
void __gui_playlist_row_expanded(GtkTreeView *treeview, GtkTreeIter *iter,
|
||||
GtkTreePath *path, gpointer data)
|
||||
{
|
||||
gui_sidebar_filter_row_expanded(iter, true);
|
||||
}
|
||||
|
||||
void __gui_playlist_drag_data_received(GtkTreeView *treeview, GdkDragContext *context,
|
||||
gint x, gint y, GtkSelectionData *data,
|
||||
guint info, guint time, gpointer user_data)
|
||||
{
|
||||
struct playlist *playlist;
|
||||
GtkTreeIter iter;
|
||||
|
||||
if (gui_sidebar_iter_from_xy(x, y, &iter))
|
||||
playlist = gui_sidebar_iter_playlist(&iter);
|
||||
if (!playlist)
|
||||
playlist = gui_pl_user_add_dialog();
|
||||
if (!playlist)
|
||||
goto out;
|
||||
|
||||
if (playlist == playlist_lookup(PL_SYSTEM, "Collection") &&
|
||||
gui_model_get_playlist() == playlist_lookup(PL_SYSTEM, "Hidden"))
|
||||
__gui_playlist_delete(NULL, NULL);
|
||||
else if (playlist != playlist_lookup(PL_SYSTEM, "History"))
|
||||
__gui_playlist_add_selected_to(playlist);
|
||||
|
||||
out:
|
||||
g_signal_stop_emission_by_name(treeview, "drag_data_received");
|
||||
gtk_drag_finish(context, true, true, time);
|
||||
}
|
||||
|
||||
bool __gui_playlist_init_idle()
|
||||
{
|
||||
struct playlist *playlist = playlist_current();
|
||||
GtkTreeModel *filter = GTK_TREE_MODEL(gui_sidebar_filter());
|
||||
GtkTreeIter iter;
|
||||
|
||||
gtk_tree_model_get_iter_first(filter, &iter);
|
||||
do {
|
||||
gui_sidebar_filter_set_expand(&iter);
|
||||
} while (gtk_tree_model_iter_next(filter, &iter));
|
||||
|
||||
select_playlist[playlist->pl_type](playlist);
|
||||
return true;
|
||||
}
|
||||
|
||||
void gui_playlist_init()
|
||||
{
|
||||
gui_pl_system_init();
|
||||
gui_pl_artist_init();
|
||||
gui_pl_user_init();
|
||||
gui_pl_library_init();
|
||||
|
||||
idle_schedule(IDLE_SYNC, __gui_playlist_init_idle, NULL);
|
||||
}
|
194
gui/playlist.cpp
194
gui/playlist.cpp
|
@ -1,194 +0,0 @@
|
|||
/*
|
||||
* Copyright 2014 (c) Anna Schumaker.
|
||||
*/
|
||||
#include <core/audio.h>
|
||||
#include <core/playlist.h>
|
||||
#include <gui/tabs.h>
|
||||
#include <gui/queue/label.h>
|
||||
|
||||
|
||||
static Gtk::ToggleButton *o_ban;
|
||||
static Gtk::ToggleButton *o_fav;
|
||||
|
||||
|
||||
|
||||
static class PlaylistColumns : public Gtk::TreeModelColumnRecord {
|
||||
public:
|
||||
Gtk::TreeModelColumn<Glib::ustring> p_name;
|
||||
PlaylistColumns() { add(p_name); }
|
||||
} p_cols;
|
||||
|
||||
|
||||
|
||||
class PlaylistWindow : public Gtk::ScrolledWindow {
|
||||
private:
|
||||
Glib::RefPtr<Gtk::Builder> _builder;
|
||||
|
||||
public:
|
||||
Gtk::TreeView *p_treeview;
|
||||
Glib::RefPtr<Gtk::ListStore> p_list;
|
||||
|
||||
PlaylistWindow(BaseObjectType *cobject,
|
||||
const Glib::RefPtr<Gtk::Builder> builder)
|
||||
: Gtk::ScrolledWindow(cobject), _builder(builder)
|
||||
{
|
||||
_builder->get_widget("p_treeview", p_treeview);
|
||||
p_list = Glib::RefPtr<Gtk::ListStore>::cast_static(_builder->get_object("p_list"));
|
||||
|
||||
p_treeview->signal_cursor_changed().connect(sigc::mem_fun(*this,
|
||||
&PlaylistWindow::on_cursor_changed));
|
||||
p_treeview->signal_button_press_event().connect(sigc::mem_fun(*this,
|
||||
&PlaylistWindow::on_clicked));
|
||||
}
|
||||
|
||||
~PlaylistWindow() {}
|
||||
|
||||
Glib::ustring current_playlist()
|
||||
{
|
||||
Gtk::TreePath path;
|
||||
Gtk::TreeModel::Row row;
|
||||
Gtk::TreeViewColumn *col;
|
||||
|
||||
p_treeview->get_cursor(path, col);
|
||||
row = *(p_list->get_iter(path));
|
||||
return row[p_cols.p_name];
|
||||
}
|
||||
|
||||
void on_cursor_changed()
|
||||
{
|
||||
playlist :: select(current_playlist());
|
||||
}
|
||||
|
||||
bool on_clicked(GdkEventButton *button)
|
||||
{
|
||||
return button->button == 3;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
static class PlaylistTab : public Tab {
|
||||
private:
|
||||
Gtk::HBox playlist_hbox;
|
||||
|
||||
PlaylistLabel *playlist_label;
|
||||
PlaylistWindow *playlist_window;
|
||||
|
||||
public:
|
||||
PlaylistTab() : Tab(playlist :: get_queue())
|
||||
{
|
||||
tab_builder->add_from_file(gui :: share_file("QueueLabel.ui"));
|
||||
tab_builder->add_from_file(gui :: share_file("PlaylistWindow.ui"));
|
||||
|
||||
tab_builder->get_widget_derived("PlaylistLabel", playlist_label);
|
||||
tab_builder->get_widget_derived("PlaylistWindow", playlist_window);
|
||||
|
||||
tab_toolbar->init(tab_pq, playlist_label, tab_window, 0);
|
||||
playlist_label->init(tab_pq);
|
||||
tab_label = playlist_label;
|
||||
|
||||
playlist_hbox.pack_start(*playlist_window, false, true);
|
||||
playlist_hbox.pack_start(*tab_window, true, true);
|
||||
playlist_hbox.show();
|
||||
|
||||
tab_vbox.pack_start(playlist_hbox, true, true);
|
||||
|
||||
gui :: get_widget<Gtk::Notebook>("o_notebook")->insert_page(tab_vbox, *playlist_label, 0);
|
||||
}
|
||||
|
||||
~PlaylistTab()
|
||||
{
|
||||
tab_unmap();
|
||||
}
|
||||
|
||||
bool on_key_press_event(const std::string &key)
|
||||
{
|
||||
std::string cur;
|
||||
std::vector<unsigned int> ids;
|
||||
|
||||
if (key != "Delete")
|
||||
return Tab :: on_key_press_event(key);
|
||||
|
||||
cur = playlist_window->current_playlist();
|
||||
if (cur == "")
|
||||
return true;
|
||||
|
||||
tab_selected_ids(ids);
|
||||
for (unsigned int i = 0; i < ids.size(); i++)
|
||||
playlist :: del(track_get(ids[i]), cur);
|
||||
return true;
|
||||
}
|
||||
} *p_tab;
|
||||
|
||||
|
||||
|
||||
static void on_ban()
|
||||
{
|
||||
struct track *track = audio :: current_track();
|
||||
if (o_ban->get_active()) {
|
||||
if (!playlist :: has(track, "Banned")) {
|
||||
playlist :: add(track, "Banned");
|
||||
audio :: next();
|
||||
}
|
||||
} else
|
||||
playlist :: del(track, "Banned");
|
||||
}
|
||||
|
||||
static void on_favorite()
|
||||
{
|
||||
struct track *track = audio :: current_track();
|
||||
if (o_fav->get_active())
|
||||
playlist :: add(track, "Favorites");
|
||||
else
|
||||
playlist :: del(track, "Favorites");
|
||||
}
|
||||
|
||||
static void playlist_added(struct queue *queue, unsigned int pos)
|
||||
{
|
||||
if (p_tab)
|
||||
p_tab->on_track_added(pos);
|
||||
}
|
||||
|
||||
static void playlist_removed(struct queue *queue, unsigned int pos)
|
||||
{
|
||||
p_tab->on_track_removed(pos);
|
||||
}
|
||||
|
||||
static void playlist_cleared(struct queue *queue, unsigned int n)
|
||||
{
|
||||
p_tab->on_tracks_cleared(n);
|
||||
}
|
||||
|
||||
static void playlist_updated(struct queue *queue, unsigned int pos)
|
||||
{
|
||||
p_tab->on_track_updated(pos);
|
||||
}
|
||||
|
||||
struct queue_ops playlist_ops = {
|
||||
playlist_added,
|
||||
playlist_removed,
|
||||
playlist_cleared,
|
||||
NULL,
|
||||
playlist_updated,
|
||||
};
|
||||
|
||||
void plist :: track_loaded(struct track *track)
|
||||
{
|
||||
if (p_tab) {
|
||||
o_ban->set_active(playlist :: has(track, "Banned"));
|
||||
o_fav->set_active(playlist :: has(track, "Favorites"));
|
||||
}
|
||||
}
|
||||
|
||||
void plist :: init()
|
||||
{
|
||||
p_tab = new PlaylistTab;
|
||||
|
||||
o_ban = gui :: get_widget<Gtk::ToggleButton>("o_ban");
|
||||
o_fav = gui :: get_widget<Gtk::ToggleButton>("o_favorite");
|
||||
|
||||
track_loaded(audio :: current_track());
|
||||
|
||||
o_ban->signal_toggled().connect(sigc::ptr_fun(on_ban));
|
||||
o_fav->signal_toggled().connect(sigc::ptr_fun(on_favorite));
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright 2016 (c) Anna Schumaker.
|
||||
*/
|
||||
#include <core/idle.h>
|
||||
#include <core/playlist.h>
|
||||
#include <gui/sidebar.h>
|
||||
|
||||
static bool __gui_pl_artist_header(GtkTreeIter *iter)
|
||||
{
|
||||
if (gui_sidebar_iter_first(iter))
|
||||
return gui_sidebar_iter_find(iter, "Collection", PL_SYSTEM);
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool __gui_pl_artist_init_idle()
|
||||
{
|
||||
struct db_entry *artist, *next;
|
||||
struct playlist *playlist;
|
||||
GtkTreeIter iter;
|
||||
|
||||
if (!__gui_pl_artist_header(&iter))
|
||||
return false;
|
||||
|
||||
db_for_each(artist, next, artist_db_get()) {
|
||||
playlist = ARTIST(artist)->ar_playlist;
|
||||
gui_sidebar_iter_sort_child(&iter, playlist, "system-users");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void gui_pl_artist_init()
|
||||
{
|
||||
idle_schedule(IDLE_SYNC, __gui_pl_artist_init_idle, NULL);
|
||||
}
|
||||
|
||||
bool gui_pl_artist_add(struct playlist *playlist)
|
||||
{
|
||||
GtkTreeIter iter;
|
||||
|
||||
if (!__gui_pl_artist_header(&iter))
|
||||
return false;
|
||||
|
||||
gui_sidebar_iter_sort_child(&iter, playlist, "system-users");
|
||||
return true;
|
||||
}
|
||||
|
||||
void gui_pl_artist_update(struct playlist *playlist)
|
||||
{
|
||||
GtkTreeIter iter, child;
|
||||
|
||||
if (!__gui_pl_artist_header(&iter))
|
||||
return;
|
||||
if (!gui_sidebar_iter_down(&iter, &child))
|
||||
return;
|
||||
if (gui_sidebar_iter_find(&child, playlist->pl_name, playlist->pl_type))
|
||||
gui_sidebar_iter_update(&child);
|
||||
}
|
||||
|
||||
void gui_pl_artist_select(struct playlist *playlist)
|
||||
{
|
||||
GtkTreeIter iter, child;
|
||||
|
||||
if (!__gui_pl_artist_header(&iter))
|
||||
return;
|
||||
if (!gui_sidebar_iter_down(&iter, &child))
|
||||
return;
|
||||
if (gui_sidebar_iter_find(&child, playlist->pl_name, playlist->pl_type))
|
||||
gui_sidebar_iter_select(&child);
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* Copyright 2015 (c) Anna Schumaker.
|
||||
*/
|
||||
#include <core/idle.h>
|
||||
#include <core/playlist.h>
|
||||
#include <gui/idle.h>
|
||||
#include <gui/playlists/library.h>
|
||||
#include <gui/sidebar.h>
|
||||
#include <gui/window.h>
|
||||
#include <glib/gi18n.h>
|
||||
|
||||
void __gui_pl_library_choose(GtkButton *button, gpointer data)
|
||||
{
|
||||
GtkFileFilter *filter;
|
||||
const gchar *music;
|
||||
GtkWidget *dialog;
|
||||
gchar *path;
|
||||
gint res;
|
||||
|
||||
filter = gtk_file_filter_new();
|
||||
gtk_file_filter_add_mime_type(filter, "inode/directory");
|
||||
|
||||
dialog = gtk_file_chooser_dialog_new("Add Music", gui_window(),
|
||||
GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER,
|
||||
_("_Cancel"), GTK_RESPONSE_CANCEL,
|
||||
_("_Open"), GTK_RESPONSE_ACCEPT,
|
||||
NULL);
|
||||
gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT);
|
||||
gtk_file_chooser_set_filter(GTK_FILE_CHOOSER(dialog), filter);
|
||||
|
||||
music = g_get_user_special_dir(G_USER_DIRECTORY_MUSIC);
|
||||
if (music)
|
||||
gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog),
|
||||
music);
|
||||
|
||||
res = gtk_dialog_run(GTK_DIALOG(dialog));
|
||||
if (res == GTK_RESPONSE_ACCEPT) {
|
||||
path = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));
|
||||
gui_pl_library_add(path);
|
||||
g_free(path);
|
||||
}
|
||||
|
||||
gtk_widget_destroy(dialog);
|
||||
}
|
||||
|
||||
static bool __gui_pl_library_header(GtkTreeIter *iter)
|
||||
{
|
||||
if (gui_sidebar_iter_first(iter))
|
||||
return gui_sidebar_iter_find(iter, "Library", PL_MAX_TYPE);
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool __gui_pl_library_init_idle()
|
||||
{
|
||||
struct db_entry *library, *next;
|
||||
struct playlist *playlist;
|
||||
GtkTreeIter iter;
|
||||
|
||||
if (!__gui_pl_library_header(&iter))
|
||||
return false;
|
||||
|
||||
db_for_each(library, next, library_db_get()) {
|
||||
playlist = LIBRARY(library)->li_playlist;
|
||||
gui_sidebar_iter_sort_child(&iter, playlist, "folder");
|
||||
}
|
||||
|
||||
#ifndef CONFIG_TESTING
|
||||
if (library_db_get()->db_size == 0)
|
||||
__gui_pl_library_choose(NULL, NULL);
|
||||
#endif /* CONFIG_TESTING */
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void gui_pl_library_init()
|
||||
{
|
||||
idle_schedule(IDLE_SYNC, __gui_pl_library_init_idle, NULL);
|
||||
}
|
||||
|
||||
struct playlist *gui_pl_library_add(const gchar *filename)
|
||||
{
|
||||
struct playlist *playlist;
|
||||
GtkTreeIter iter;
|
||||
|
||||
if (!__gui_pl_library_header(&iter))
|
||||
return false;
|
||||
|
||||
playlist = playlist_new(PL_LIBRARY, filename);
|
||||
if (playlist) {
|
||||
gui_sidebar_iter_sort_child(&iter, playlist, "folder");
|
||||
gui_idle_enable();
|
||||
}
|
||||
return playlist;
|
||||
}
|
||||
|
||||
void gui_pl_library_update(struct playlist *playlist)
|
||||
{
|
||||
GtkTreeIter iter, child;
|
||||
|
||||
if (!__gui_pl_library_header(&iter))
|
||||
return;
|
||||
if (!gui_sidebar_iter_down(&iter, &child))
|
||||
return;
|
||||
if (gui_sidebar_iter_find(&child, playlist->pl_name, playlist->pl_type))
|
||||
gui_sidebar_iter_update(&child);
|
||||
}
|
||||
|
||||
void gui_pl_library_select(struct playlist *playlist)
|
||||
{
|
||||
GtkTreeIter iter, child;
|
||||
if (!__gui_pl_library_header(&iter))
|
||||
return;
|
||||
if (!gui_sidebar_iter_down(&iter, &child))
|
||||
return;
|
||||
if (gui_sidebar_iter_find(&child, playlist->pl_name, playlist->pl_type))
|
||||
gui_sidebar_iter_select(&child);
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
/*
|
||||
* Copyright 2016 (c) Anna Schumaker.
|
||||
*/
|
||||
#include <core/audio.h>
|
||||
#include <core/idle.h>
|
||||
#include <core/playlist.h>
|
||||
#include <core/string.h>
|
||||
#include <gui/playlists/system.h>
|
||||
#include <gui/sidebar.h>
|
||||
|
||||
static struct playlist *favorites;
|
||||
static struct playlist *hidden;
|
||||
|
||||
static bool __gui_pl_system_is_playlist(struct playlist *playlist)
|
||||
{
|
||||
return string_match(playlist->pl_name, "Favorites") ||
|
||||
string_match(playlist->pl_name, "Hidden");
|
||||
}
|
||||
|
||||
static bool __gui_pl_system_is_dynamic(struct playlist *playlist)
|
||||
{
|
||||
return string_match(playlist->pl_name, "Most Played") ||
|
||||
string_match(playlist->pl_name, "Least Played") ||
|
||||
string_match(playlist->pl_name, "Unplayed");
|
||||
}
|
||||
|
||||
static bool __gui_pl_system_find_descend_header(GtkTreeIter *iter,
|
||||
const gchar *name)
|
||||
{
|
||||
GtkTreeIter header;
|
||||
|
||||
if (!gui_sidebar_iter_first(&header))
|
||||
return false;
|
||||
if (!gui_sidebar_iter_find(&header, name, PL_MAX_TYPE))
|
||||
return false;
|
||||
return gui_sidebar_iter_down(&header, iter);
|
||||
}
|
||||
|
||||
void __gui_pl_system_favorite_toggled(GtkToggleButton *toggle, gpointer data)
|
||||
{
|
||||
if (gtk_toggle_button_get_active(toggle))
|
||||
playlist_add(favorites, audio_cur_track());
|
||||
else
|
||||
playlist_remove(favorites, audio_cur_track());
|
||||
}
|
||||
|
||||
void __gui_pl_system_hide_toggled(GtkToggleButton *toggle, gpointer data)
|
||||
{
|
||||
if (gtk_toggle_button_get_active(toggle)) {
|
||||
if (playlist_add(hidden, audio_cur_track()))
|
||||
audio_next();
|
||||
} else
|
||||
playlist_remove(hidden, audio_cur_track());
|
||||
}
|
||||
|
||||
static bool __gui_pl_system_init_idle()
|
||||
{
|
||||
GtkTreeIter iter;
|
||||
|
||||
/* Add toplevel playlists. */
|
||||
gui_sidebar_iter_first(&iter);
|
||||
gui_sidebar_iter_add(&iter, playlist_lookup(PL_SYSTEM, "Queued Tracks"),
|
||||
"audio-x-generic");
|
||||
gui_sidebar_iter_add(&iter, playlist_lookup(PL_SYSTEM, "Collection"),
|
||||
"media-optical");
|
||||
gui_sidebar_iter_add(&iter, playlist_lookup(PL_SYSTEM, "History"),
|
||||
"document-open-recent");
|
||||
|
||||
/* Add user-modifiable playlists. */
|
||||
gui_sidebar_iter_find(&iter, "Playlists", PL_MAX_TYPE);
|
||||
gui_sidebar_iter_append_child(&iter, playlist_lookup(PL_SYSTEM, "Favorites"),
|
||||
"emblem-favorite");
|
||||
gui_sidebar_iter_append_child(&iter, playlist_lookup(PL_SYSTEM, "Hidden"),
|
||||
"window-close");
|
||||
|
||||
/* Add dynamic playlists. */
|
||||
gui_sidebar_iter_find(&iter, "Dynamic", PL_MAX_TYPE);
|
||||
gui_sidebar_iter_append_child(&iter, playlist_lookup(PL_SYSTEM, "Most Played"),
|
||||
"go-up");
|
||||
gui_sidebar_iter_append_child(&iter, playlist_lookup(PL_SYSTEM, "Least Played"),
|
||||
"go-down");
|
||||
gui_sidebar_iter_append_child(&iter, playlist_lookup(PL_SYSTEM, "Unplayed"),
|
||||
"audio-x-generic");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void gui_pl_system_init()
|
||||
{
|
||||
favorites = playlist_lookup(PL_SYSTEM, "Favorites");
|
||||
hidden = playlist_lookup(PL_SYSTEM, "Hidden");
|
||||
idle_schedule(IDLE_SYNC, __gui_pl_system_init_idle, NULL);
|
||||
}
|
||||
|
||||
void gui_pl_system_update(struct playlist *playlist)
|
||||
{
|
||||
GtkTreeIter iter;
|
||||
|
||||
if (__gui_pl_system_is_playlist(playlist)) {
|
||||
if (!__gui_pl_system_find_descend_header(&iter, "Playlists"))
|
||||
return;
|
||||
} else if (__gui_pl_system_is_dynamic(playlist)) {
|
||||
if (!__gui_pl_system_find_descend_header(&iter, "Dynamic"))
|
||||
return;
|
||||
} else {
|
||||
if (!gui_sidebar_iter_first(&iter))
|
||||
return;
|
||||
}
|
||||
|
||||
if (gui_sidebar_iter_find(&iter, playlist->pl_name, playlist->pl_type))
|
||||
gui_sidebar_iter_update(&iter);
|
||||
}
|
||||
|
||||
void gui_pl_system_select(struct playlist *playlist)
|
||||
{
|
||||
GtkTreeIter iter;
|
||||
|
||||
if (__gui_pl_system_is_playlist(playlist)) {
|
||||
if (!__gui_pl_system_find_descend_header(&iter, "Playlists"))
|
||||
return;
|
||||
} else if (__gui_pl_system_is_dynamic(playlist)) {
|
||||
if (!__gui_pl_system_find_descend_header(&iter, "Dynamic"))
|
||||
return;
|
||||
} else {
|
||||
if (!gui_sidebar_iter_first(&iter))
|
||||
return;
|
||||
}
|
||||
|
||||
if (gui_sidebar_iter_find(&iter, playlist->pl_name, playlist->pl_type))
|
||||
gui_sidebar_iter_select(&iter);
|
||||
}
|
||||
|
||||
void gui_pl_system_track_loaded(struct track *track)
|
||||
{
|
||||
gtk_toggle_button_set_active(gui_favorite_button(),
|
||||
playlist_has(favorites, track));
|
||||
gtk_toggle_button_set_active(gui_hide_button(),
|
||||
playlist_has(hidden, track));
|
||||
}
|
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
* Copyright 2016 (c) Anna Schumaker.
|
||||
*/
|
||||
#include <core/idle.h>
|
||||
#include <core/playlist.h>
|
||||
#include <gui/sidebar.h>
|
||||
#include <gui/window.h>
|
||||
#include <glib/gi18n.h>
|
||||
|
||||
static bool __gui_pl_user_header(GtkTreeIter *iter)
|
||||
{
|
||||
if (gui_sidebar_iter_first(iter))
|
||||
return gui_sidebar_iter_find(iter, "Playlists", PL_MAX_TYPE);
|
||||
return false;
|
||||
}
|
||||
|
||||
static gint __gui_pl_user_compare(gconstpointer a, gconstpointer b)
|
||||
{
|
||||
struct playlist *pl_a = (struct playlist *)a;
|
||||
struct playlist *pl_b = (struct playlist *)b;
|
||||
return g_utf8_collate(pl_a->pl_name, pl_b->pl_name);
|
||||
}
|
||||
|
||||
static bool __gui_pl_user_init_idle()
|
||||
{
|
||||
struct db_entry *user, *next;
|
||||
struct playlist *playlist;
|
||||
GtkTreeIter iter;
|
||||
|
||||
if (!__gui_pl_user_header(&iter))
|
||||
return false;
|
||||
|
||||
db_for_each(user, next, pl_user_db_get()) {
|
||||
playlist = &USER_PLAYLIST(user)->pl_playlist;
|
||||
gui_sidebar_iter_sort_child(&iter, playlist, "text-x-generic");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void __gui_pl_user_editing_started(GtkCellRenderer *renderer,
|
||||
GtkCellEditable *editable,
|
||||
gchar *path, gpointer data)
|
||||
{
|
||||
struct playlist *playlist;
|
||||
GtkTreeIter iter;
|
||||
|
||||
if (!gui_sidebar_iter_from_string(path, &iter))
|
||||
return;
|
||||
|
||||
playlist = gui_sidebar_iter_playlist(&iter);
|
||||
if (GTK_IS_ENTRY(editable))
|
||||
gtk_entry_set_text(GTK_ENTRY(editable), playlist->pl_name);
|
||||
}
|
||||
|
||||
void __gui_pl_user_edited(GtkCellRendererText *renderer, gchar *path,
|
||||
gchar *new_name, gpointer data)
|
||||
{
|
||||
struct playlist *playlist;
|
||||
GtkTreeIter iter;
|
||||
|
||||
if (!gui_sidebar_iter_from_string(path, &iter))
|
||||
return;
|
||||
|
||||
playlist = gui_sidebar_iter_playlist(&iter);
|
||||
pl_user_rename(playlist, new_name);
|
||||
gui_sidebar_iter_update_playlist(&iter, playlist);
|
||||
gui_sidebar_iter_set_editable(&iter, false);
|
||||
}
|
||||
|
||||
void gui_pl_user_init()
|
||||
{
|
||||
idle_schedule(IDLE_SYNC, __gui_pl_user_init_idle, NULL);
|
||||
}
|
||||
|
||||
struct playlist *gui_pl_user_add(const gchar *name)
|
||||
{
|
||||
struct playlist *playlist;
|
||||
GtkTreeIter iter;
|
||||
|
||||
if (!__gui_pl_user_header(&iter))
|
||||
return NULL;
|
||||
|
||||
playlist = playlist_new(PL_USER, name);
|
||||
if (playlist)
|
||||
gui_sidebar_iter_sort_child(&iter, playlist, "text-x-generic");
|
||||
return playlist;
|
||||
}
|
||||
|
||||
struct playlist *gui_pl_user_add_dialog(void)
|
||||
{
|
||||
unsigned int flags = GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL;
|
||||
GtkWidget *entry, *dialog, *content;
|
||||
struct playlist *playlist = NULL;
|
||||
|
||||
entry = gtk_entry_new();
|
||||
dialog = gtk_dialog_new_with_buttons("New Playlist Name?",
|
||||
gui_window(), flags,
|
||||
_("_Cancel"), GTK_RESPONSE_CANCEL,
|
||||
_("_OK"), GTK_RESPONSE_ACCEPT,
|
||||
NULL);
|
||||
content = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
|
||||
|
||||
gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT);
|
||||
gtk_entry_set_activates_default(GTK_ENTRY(entry), true);
|
||||
gtk_container_add(GTK_CONTAINER(content), entry);
|
||||
gtk_widget_show_all(dialog);
|
||||
|
||||
if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT)
|
||||
playlist = gui_pl_user_add(gtk_entry_get_text(GTK_ENTRY(entry)));
|
||||
gtk_widget_destroy(dialog);
|
||||
|
||||
return playlist;
|
||||
}
|
||||
|
||||
void gui_pl_user_update(struct playlist *playlist)
|
||||
{
|
||||
GtkTreeIter iter, child;
|
||||
|
||||
if (!__gui_pl_user_header(&iter))
|
||||
return;
|
||||
if (!gui_sidebar_iter_down(&iter, &child))
|
||||
return;
|
||||
if (gui_sidebar_iter_find(&child, playlist->pl_name, playlist->pl_type))
|
||||
gui_sidebar_iter_update(&child);
|
||||
}
|
||||
|
||||
void gui_pl_user_select(struct playlist *playlist)
|
||||
{
|
||||
GtkTreeIter iter, child;
|
||||
|
||||
if (!__gui_pl_user_header(&iter))
|
||||
return;
|
||||
if (!gui_sidebar_iter_down(&iter, &child))
|
||||
return;
|
||||
if (gui_sidebar_iter_find(&child, playlist->pl_name, playlist->pl_type))
|
||||
gui_sidebar_iter_select(&child);
|
||||
}
|
||||
|
||||
GList *gui_pl_user_list(void)
|
||||
{
|
||||
struct db_entry *user, *next;
|
||||
struct playlist *playlist;
|
||||
GList *list = NULL;
|
||||
|
||||
db_for_each(user, next, pl_user_db_get()) {
|
||||
playlist = &USER_PLAYLIST(user)->pl_playlist;
|
||||
list = g_list_insert_sorted(list, playlist,
|
||||
__gui_pl_user_compare);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
228
gui/queue.cpp
228
gui/queue.cpp
|
@ -1,228 +0,0 @@
|
|||
/*
|
||||
* Copyright 2014 (c) Anna Schumaker.
|
||||
*/
|
||||
#include <core/deck.h>
|
||||
#include <core/string.h>
|
||||
#include <gui/tabs.h>
|
||||
#include <gui/queue/label.h>
|
||||
|
||||
|
||||
static void renumber_queues();
|
||||
|
||||
static class QueueColumns : public Gtk::TreeModelColumnRecord {
|
||||
public:
|
||||
QueueColumns()
|
||||
{ add(q_col_track); add(q_col_title); add(q_col_length);
|
||||
add(q_col_artist); add(q_col_album); add(q_col_year);
|
||||
add(q_col_genre); add(q_col_count); add(q_col_played);
|
||||
add(q_col_path); }
|
||||
|
||||
Gtk::TreeModelColumn<unsigned int> q_col_track;
|
||||
Gtk::TreeModelColumn<std::string> q_col_title;
|
||||
Gtk::TreeModelColumn<std::string> q_col_length;
|
||||
Gtk::TreeModelColumn<std::string> q_col_artist;
|
||||
Gtk::TreeModelColumn<std::string> q_col_album;
|
||||
Gtk::TreeModelColumn<unsigned int> q_col_year;
|
||||
Gtk::TreeModelColumn<std::string> q_col_genre;
|
||||
Gtk::TreeModelColumn<unsigned int> q_col_count;
|
||||
Gtk::TreeModelColumn<std::string> q_col_played;
|
||||
Gtk::TreeModelColumn<std::string> q_col_path;
|
||||
} queue_cols;
|
||||
|
||||
|
||||
|
||||
class QueueTab : public Tab {
|
||||
private:
|
||||
/**
|
||||
* Queue tab widgets
|
||||
*/
|
||||
TempLabel *q_label;
|
||||
|
||||
public:
|
||||
QueueTab(queue *, unsigned int num);
|
||||
~QueueTab();
|
||||
|
||||
void on_track_removed(unsigned int);
|
||||
|
||||
/**
|
||||
* Helper functions
|
||||
*/
|
||||
bool on_key_press_event(const std::string &);
|
||||
void queue_set_number(unsigned int);
|
||||
void on_tab_reordered();
|
||||
void on_move_queue(int);
|
||||
|
||||
/**
|
||||
* GTK-MM Callbacks
|
||||
*/
|
||||
void on_close_clicked();
|
||||
};
|
||||
|
||||
static std::map<Gtk::Widget *, QueueTab *> queue_mapping;
|
||||
|
||||
|
||||
QueueTab :: QueueTab(queue *pq, unsigned int num)
|
||||
: Tab(pq)
|
||||
{
|
||||
tab_builder->add_from_file(gui :: share_file("QueueLabel.ui"));
|
||||
|
||||
/*
|
||||
* Create our tab widget
|
||||
*/
|
||||
tab_builder->get_widget_derived("TempLabel", q_label);
|
||||
|
||||
q_label->temp_close->signal_clicked().connect(sigc::mem_fun(*this,
|
||||
&QueueTab::on_close_clicked));
|
||||
q_label->init(tab_pq);
|
||||
tab_label = q_label;
|
||||
|
||||
queue_set_number(num);
|
||||
|
||||
|
||||
/*
|
||||
* Now set up the toolbar
|
||||
*/
|
||||
tab_toolbar->init(pq, q_label, tab_window, T_RANDOM | T_REPEAT | T_SWITCH);
|
||||
|
||||
|
||||
/*
|
||||
* Fill in the page!
|
||||
*/
|
||||
|
||||
tab_vbox.pack_start(*tab_window, true, true);
|
||||
|
||||
queue_mapping[&tab_vbox] = this;
|
||||
|
||||
gui :: get_widget<Gtk::Notebook>("o_notebook")->insert_page(tab_vbox, *q_label, num);
|
||||
gui :: get_widget<Gtk::Notebook>("o_notebook")->set_tab_reorderable(tab_vbox);
|
||||
}
|
||||
|
||||
QueueTab :: ~QueueTab()
|
||||
{
|
||||
queue_mapping.erase(&tab_vbox);
|
||||
tab_unmap();
|
||||
gui :: get_widget<Gtk::Notebook>("o_notebook")->remove_page(tab_vbox);
|
||||
renumber_queues();
|
||||
}
|
||||
|
||||
void QueueTab :: on_track_removed(unsigned int row)
|
||||
{
|
||||
this->Tab :: on_track_removed(row);
|
||||
if (queue_size(tab_pq) == 0)
|
||||
delete this;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* Queue tab helper functions
|
||||
*
|
||||
*/
|
||||
|
||||
bool QueueTab :: on_key_press_event(const std::string &key)
|
||||
{
|
||||
std::vector<unsigned int> ids;
|
||||
|
||||
if (key != "Delete")
|
||||
return Tab :: on_key_press_event(key);
|
||||
|
||||
/*
|
||||
* tab_selected_ids() returns track_ids and not
|
||||
* playlist_ids,so we can't use it here =(
|
||||
*/
|
||||
Glib::RefPtr<Gtk::TreeSelection> sel = tab_window->q_treeview->get_selection();
|
||||
std::vector<Gtk::TreeModel::Path> rows = sel->get_selected_rows();
|
||||
Gtk::TreeModel::Path path;
|
||||
|
||||
for (unsigned int i = 0; i < rows.size(); i++) {
|
||||
path = tab_window->q_filter->convert_path_to_child_path(rows[i]);
|
||||
ids.push_back(path[0]);
|
||||
}
|
||||
|
||||
for (unsigned int i = ids.size(); i > 0; i--)
|
||||
queue_remove(tab_pq, ids[i-1]);
|
||||
return true;
|
||||
}
|
||||
|
||||
void QueueTab :: queue_set_number(unsigned int num)
|
||||
{
|
||||
gchar *text = g_strdup_printf("%u. ", num);
|
||||
q_label->temp_number->set_text(text);
|
||||
g_free(text);
|
||||
}
|
||||
|
||||
void QueueTab :: on_tab_reordered()
|
||||
{
|
||||
queue_set_number(tab_page_num());
|
||||
}
|
||||
|
||||
void QueueTab :: on_move_queue(int num)
|
||||
{
|
||||
deck :: move(tab_pq, num);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* Gtk-MM Callback Functions
|
||||
*
|
||||
*/
|
||||
|
||||
void QueueTab :: on_close_clicked()
|
||||
{
|
||||
deck :: destroy(tab_pq);
|
||||
delete this;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* Global functions
|
||||
*
|
||||
*/
|
||||
static void renumber_queues()
|
||||
{
|
||||
std::map<Gtk::Widget *, QueueTab *>::iterator it;
|
||||
for (it = queue_mapping.begin(); it != queue_mapping.end(); it++)
|
||||
it->second->on_tab_reordered();
|
||||
}
|
||||
|
||||
void on_pq_created(queue *pq, unsigned int num)
|
||||
{
|
||||
new QueueTab(pq, num);
|
||||
}
|
||||
|
||||
static void on_page_reordered(Gtk::Widget *page, int num)
|
||||
{
|
||||
Gtk::Notebook *notebook = gui :: get_widget<Gtk::Notebook>("o_notebook");
|
||||
|
||||
if ((unsigned int)num >= deck :: get_queues().size()) {
|
||||
notebook->reorder_child(*page, deck::get_queues().size() - 1);
|
||||
return;
|
||||
}
|
||||
|
||||
std::map<Gtk::Widget *, QueueTab *>::iterator it;
|
||||
it = queue_mapping.find(page);
|
||||
if (it != queue_mapping.end()) {
|
||||
it->second->on_move_queue(num);
|
||||
renumber_queues();
|
||||
}
|
||||
}
|
||||
|
||||
void init_queue_tabs()
|
||||
{
|
||||
Gtk::Notebook *notebook = gui :: get_widget<Gtk::Notebook>("o_notebook");
|
||||
notebook->signal_page_reordered().connect(sigc::ptr_fun(on_page_reordered));
|
||||
}
|
||||
|
||||
void post_init_queue_tabs()
|
||||
{
|
||||
std::list<TempQueue>::iterator it;
|
||||
unsigned int i = 0;
|
||||
|
||||
for (it = deck :: get_queues().begin(); it != deck :: get_queues().end(); it++)
|
||||
on_pq_created(&(*it), i++);
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
res = Glob("*.cpp")
|
||||
Return("res")
|
|
@ -1,87 +0,0 @@
|
|||
/*
|
||||
* Copyright 2015 (c) Anna Schumaker.
|
||||
*/
|
||||
#include <core/string.h>
|
||||
#include <gui/queue/label.h>
|
||||
|
||||
|
||||
|
||||
QueueLabel :: QueueLabel(BaseObjectType *cobject,
|
||||
const Glib::RefPtr<Gtk::Builder> builder)
|
||||
: Gtk::HBox(cobject), _builder(builder)
|
||||
{
|
||||
}
|
||||
|
||||
QueueLabel :: ~QueueLabel()
|
||||
{
|
||||
}
|
||||
|
||||
void QueueLabel :: init(queue *queue)
|
||||
{
|
||||
_queue = queue;
|
||||
set_size();
|
||||
}
|
||||
|
||||
|
||||
|
||||
CollectionLabel :: CollectionLabel(BaseObjectType *cobject,
|
||||
const Glib::RefPtr<Gtk::Builder> builder)
|
||||
: QueueLabel(cobject, builder)
|
||||
{
|
||||
_builder->get_widget("collection_size", collection_size);
|
||||
}
|
||||
|
||||
void CollectionLabel :: set_size()
|
||||
{
|
||||
gchar *size = g_strdup_printf("%u", queue_size(_queue));
|
||||
collection_size->set_text(size);
|
||||
g_free(size);
|
||||
}
|
||||
|
||||
|
||||
|
||||
HistoryLabel :: HistoryLabel(BaseObjectType *cobject,
|
||||
const Glib::RefPtr<Gtk::Builder> builder)
|
||||
: QueueLabel(cobject, builder)
|
||||
{
|
||||
_builder->get_widget("history_size", history_size);
|
||||
}
|
||||
|
||||
void HistoryLabel :: set_size()
|
||||
{
|
||||
gchar *size = g_strdup_printf("%u", queue_size(_queue));
|
||||
history_size->set_text(size);
|
||||
g_free(size);
|
||||
}
|
||||
|
||||
|
||||
|
||||
PlaylistLabel :: PlaylistLabel(BaseObjectType *cobject,
|
||||
const Glib::RefPtr<Gtk::Builder> builder)
|
||||
: QueueLabel(cobject, builder)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
TempLabel :: TempLabel(BaseObjectType *cobject,
|
||||
const Glib::RefPtr<Gtk::Builder> builder)
|
||||
: QueueLabel(cobject, builder)
|
||||
{
|
||||
_builder->get_widget("temp_number", temp_number);
|
||||
_builder->get_widget("temp_size", temp_size);
|
||||
_builder->get_widget("temp_close", temp_close);
|
||||
}
|
||||
|
||||
void TempLabel :: set_sensitive(bool sensitive)
|
||||
{
|
||||
temp_number->set_sensitive(sensitive);
|
||||
temp_size->set_sensitive(sensitive);
|
||||
}
|
||||
|
||||
void TempLabel :: set_size()
|
||||
{
|
||||
gchar *size = g_strdup_printf("%u", queue_size(_queue));
|
||||
temp_size->set_text(size);
|
||||
g_free(size);
|
||||
}
|
|
@ -1,194 +0,0 @@
|
|||
/*
|
||||
* Copyright 2014 (c) Anna Schumaker.
|
||||
*
|
||||
* See the example at:
|
||||
* https://git.gnome.org/browse/gtkmm-documentation/tree/examples/others/treemodelcustom
|
||||
*/
|
||||
#include <core/audio.h>
|
||||
extern "C" {
|
||||
#include <core/string.h>
|
||||
}
|
||||
#include <gui/queue/model.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
|
||||
QueueModel::QueueModel(queue *q)
|
||||
: Glib::ObjectBase( typeid(QueueModel) ),
|
||||
Glib::Object(), _stamp(1), _queue(q)
|
||||
{
|
||||
}
|
||||
|
||||
void QueueModel::increment_stamp()
|
||||
{
|
||||
if (++_stamp == 0)
|
||||
++_stamp;
|
||||
}
|
||||
|
||||
bool QueueModel::check_iter_validity(const Gtk::TreeIter &iter) const
|
||||
{
|
||||
return _stamp == iter.get_stamp();
|
||||
}
|
||||
|
||||
void QueueModel::on_row_inserted(unsigned int row)
|
||||
{
|
||||
increment_stamp();
|
||||
row_inserted(Gtk::TreePath(1, row), Gtk::TreeIter());
|
||||
}
|
||||
|
||||
void QueueModel::on_row_deleted(unsigned int row)
|
||||
{
|
||||
increment_stamp();
|
||||
row_deleted(Gtk::TreePath(1, row));
|
||||
}
|
||||
|
||||
void QueueModel::on_cleared(unsigned int n)
|
||||
{
|
||||
increment_stamp();
|
||||
for (unsigned int i = 1; i <= n; i++)
|
||||
row_deleted(Gtk::TreePath(1, n - i));
|
||||
}
|
||||
|
||||
void QueueModel::on_row_changed(unsigned int row)
|
||||
{
|
||||
increment_stamp();
|
||||
row_changed(Gtk::TreePath(1, row), Gtk::TreeIter());
|
||||
}
|
||||
|
||||
void QueueModel::on_path_selected(const Gtk::TreePath &path)
|
||||
{
|
||||
audio :: load_track(track_get(path_to_id(path)));
|
||||
queue_selected(_queue, path[0]);
|
||||
audio :: play();
|
||||
}
|
||||
|
||||
unsigned int QueueModel :: iter_to_id(const Gtk::TreeIter &iter) const
|
||||
{
|
||||
return GPOINTER_TO_UINT(iter.gobj()->user_data);
|
||||
}
|
||||
|
||||
unsigned int QueueModel::path_to_id(const Gtk::TreePath &path) const
|
||||
{
|
||||
return queue_at(_queue, path[0])->tr_dbe.dbe_index;
|
||||
}
|
||||
|
||||
|
||||
|
||||
Gtk::TreeModelFlags QueueModel::get_flags_vfunc() const
|
||||
{
|
||||
return Gtk::TREE_MODEL_LIST_ONLY;
|
||||
}
|
||||
|
||||
int QueueModel::get_n_columns_vfunc() const
|
||||
{
|
||||
return 10;
|
||||
}
|
||||
|
||||
GType QueueModel::get_column_type_vfunc(int index) const
|
||||
{
|
||||
if (index > 9)
|
||||
return 0;
|
||||
if (index == 0 || index == 5 || index == 7)
|
||||
return G_TYPE_UINT;
|
||||
return G_TYPE_STRING;
|
||||
}
|
||||
|
||||
template <class T>
|
||||
static void set_val(const T &t, Glib::ValueBase &value)
|
||||
{
|
||||
Glib::Value<T> specific;
|
||||
specific.init(Glib::Value<T>::value_type());
|
||||
specific.set(t);
|
||||
value.init(specific.gobj());
|
||||
}
|
||||
|
||||
void QueueModel::get_value_vfunc(const Gtk::TreeIter &iter, int column,
|
||||
Glib::ValueBase &value) const
|
||||
{
|
||||
struct track *track;
|
||||
std::string field;
|
||||
gchar *str;
|
||||
|
||||
if (!check_iter_validity(iter) ||
|
||||
column > get_n_columns_vfunc())
|
||||
return;
|
||||
|
||||
track = queue_at(_queue, iter_to_id(iter));
|
||||
|
||||
switch (column) {
|
||||
case 0:
|
||||
return set_val(track->tr_track, value);
|
||||
case 1:
|
||||
field = track->tr_title;
|
||||
return set_val(field, value);
|
||||
case 2:
|
||||
str = string_sec2str(track->tr_length);
|
||||
set_val(Glib::ustring(str), value);
|
||||
g_free(str);
|
||||
return;
|
||||
case 3:
|
||||
field = track->tr_artist->ar_name;
|
||||
return set_val(field, value);
|
||||
case 4:
|
||||
field = track->tr_album->al_name;
|
||||
return set_val(field, value);
|
||||
case 5:
|
||||
return set_val(track->tr_album->al_year, value);
|
||||
case 6:
|
||||
field = track->tr_genre->ge_name;
|
||||
return set_val(field, value);
|
||||
case 7:
|
||||
return set_val(track->tr_count, value);
|
||||
case 8:
|
||||
str = track_last_play(track);
|
||||
set_val(Glib::Markup::escape_text(str), value);
|
||||
g_free(str);
|
||||
return;
|
||||
case 9:
|
||||
str = track_path(track);
|
||||
set_val(Glib::Markup::escape_text(str), value);
|
||||
g_free(str);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bool QueueModel::iter_next_vfunc(const Gtk::TreeIter &iter,
|
||||
Gtk::TreeIter &next) const
|
||||
{
|
||||
next = Gtk::TreeIter();
|
||||
if (check_iter_validity(iter))
|
||||
return iter_nth_root_child_vfunc(iter_to_id(iter) + 1, next);
|
||||
return false;
|
||||
}
|
||||
|
||||
int QueueModel::iter_n_root_children_vfunc() const
|
||||
{
|
||||
return queue_size(_queue);
|
||||
}
|
||||
|
||||
bool QueueModel::iter_nth_root_child_vfunc(int n, Gtk::TreeIter &iter) const
|
||||
{
|
||||
iter = Gtk::TreeIter();
|
||||
if (n >= (int)queue_size(_queue))
|
||||
return false;
|
||||
|
||||
iter.set_stamp(_stamp);
|
||||
iter.gobj()->user_data = GUINT_TO_POINTER(n);
|
||||
return true;
|
||||
}
|
||||
|
||||
Gtk::TreeModel::Path QueueModel::get_path_vfunc(const Gtk::TreeIter &iter) const
|
||||
{
|
||||
Gtk::TreeModel::Path path;
|
||||
if (check_iter_validity(iter))
|
||||
path.push_back(iter_to_id(iter));
|
||||
return path;
|
||||
}
|
||||
|
||||
bool QueueModel::get_iter_vfunc(const Gtk::TreePath &path,
|
||||
Gtk::TreeIter &iter) const
|
||||
{
|
||||
iter = Gtk::TreeIter();
|
||||
if (path.size() == 1)
|
||||
return iter_nth_root_child_vfunc(path[0], iter);
|
||||
return false;
|
||||
}
|
|
@ -1,93 +0,0 @@
|
|||
/*
|
||||
* Copyright 2015 (c) Anna Schumaker.
|
||||
*/
|
||||
#include <gui/queue/toolbar.h>
|
||||
|
||||
QueueToolbar :: QueueToolbar(BaseObjectType *cobject,
|
||||
const Glib::RefPtr<Gtk::Builder> builder)
|
||||
: Gtk::HBox(cobject), _builder(builder)
|
||||
{
|
||||
_builder->get_widget("q_search", q_search);
|
||||
_builder->get_widget("q_switch", _q_switch);
|
||||
_builder->get_widget("q_repeat", _q_repeat);
|
||||
_builder->get_widget("q_random", _q_random);
|
||||
|
||||
q_search->signal_key_release_event().connect(sigc::mem_fun(*this,
|
||||
&QueueToolbar::on_search_key_released));
|
||||
q_search->signal_search_changed().connect(sigc::mem_fun(*this,
|
||||
&QueueToolbar::on_search_changed));
|
||||
}
|
||||
|
||||
QueueToolbar :: ~QueueToolbar()
|
||||
{
|
||||
}
|
||||
|
||||
void QueueToolbar :: init(queue *queue, QueueLabel *label,
|
||||
QueueWindow *window, unsigned int flags)
|
||||
{
|
||||
_queue = queue;
|
||||
_q_label = label;
|
||||
_q_window = window;
|
||||
|
||||
if (flags & T_SWITCH) {
|
||||
_q_switch->property_active().signal_changed().connect(
|
||||
sigc::mem_fun(*this, &QueueToolbar::on_switch_toggled));
|
||||
_q_switch->set_active(queue_has_flag(_queue, Q_ENABLED));
|
||||
_q_switch->show();
|
||||
}
|
||||
|
||||
if (flags & T_RANDOM) {
|
||||
_q_random->signal_toggled().connect(sigc::mem_fun(*this,
|
||||
&QueueToolbar::on_random_toggled));
|
||||
_q_random->set_active(queue_has_flag(_queue, Q_RANDOM));
|
||||
_q_random->show();
|
||||
}
|
||||
|
||||
if (flags & T_REPEAT) {
|
||||
_q_repeat->signal_toggled().connect(sigc::mem_fun(*this,
|
||||
&QueueToolbar::on_repeat_toggled));
|
||||
_q_repeat->set_active(queue_has_flag(_queue, Q_REPEAT));
|
||||
_q_repeat->show();
|
||||
}
|
||||
}
|
||||
|
||||
void QueueToolbar :: on_random_toggled()
|
||||
{
|
||||
if (_q_random->get_active())
|
||||
queue_set_flag(_queue, Q_RANDOM);
|
||||
else
|
||||
queue_unset_flag(_queue, Q_RANDOM);
|
||||
}
|
||||
|
||||
void QueueToolbar :: on_repeat_toggled()
|
||||
{
|
||||
if (_q_repeat->get_active())
|
||||
queue_set_flag(_queue, Q_REPEAT);
|
||||
else
|
||||
queue_unset_flag(_queue, Q_REPEAT);
|
||||
}
|
||||
|
||||
bool QueueToolbar :: on_search_key_released(GdkEventKey *event)
|
||||
{
|
||||
std::string key = gdk_keyval_name(event->keyval);
|
||||
return key == "space";
|
||||
}
|
||||
|
||||
void QueueToolbar :: on_search_changed()
|
||||
{
|
||||
std::string text = q_search->get_text();
|
||||
_q_window->filter(text);
|
||||
}
|
||||
|
||||
void QueueToolbar :: on_switch_toggled()
|
||||
{
|
||||
bool active = _q_switch->get_active();
|
||||
|
||||
if (active)
|
||||
queue_set_flag(_queue, Q_ENABLED);
|
||||
else
|
||||
queue_unset_flag(_queue, Q_ENABLED);
|
||||
|
||||
_q_label->set_sensitive(active);
|
||||
_q_window->set_sensitive(active);
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
/*
|
||||
* Copyright 2015 (c) Anna Schumaker.
|
||||
*/
|
||||
extern "C" {
|
||||
#include <core/filter.h>
|
||||
}
|
||||
#include <gui/queue/window.h>
|
||||
|
||||
QueueWindow :: QueueWindow(BaseObjectType *cobject,
|
||||
const Glib::RefPtr<Gtk::Builder> builder)
|
||||
: Gtk::ScrolledWindow(cobject), _builder(builder), _q_search_empty(true)
|
||||
{
|
||||
_q_search_res = SET_INIT();
|
||||
_builder->get_widget("q_treeview", q_treeview);
|
||||
}
|
||||
|
||||
QueueWindow :: ~QueueWindow()
|
||||
{
|
||||
set_deinit(&_q_search_res);
|
||||
}
|
||||
|
||||
void QueueWindow :: init(queue *queue)
|
||||
{
|
||||
_queue = queue;
|
||||
q_model = Glib::RefPtr<QueueModel>(new QueueModel(queue));
|
||||
q_filter = Gtk::TreeModelFilter::create(q_model);
|
||||
|
||||
q_filter->set_visible_func(sigc::mem_fun(*this, &QueueWindow::filter_ids));
|
||||
|
||||
q_treeview->signal_row_activated().connect(sigc::mem_fun(*this,
|
||||
&QueueWindow :: on_row_activated));
|
||||
q_treeview->signal_key_press_event().connect(sigc::mem_fun(*this,
|
||||
&QueueWindow :: on_key_press), false);
|
||||
q_treeview->set_model(q_filter);
|
||||
}
|
||||
|
||||
void QueueWindow :: filter(std::string &text)
|
||||
{
|
||||
_q_search_empty = (text.find_first_not_of(" \t") == std::string::npos);
|
||||
if (!_q_search_empty)
|
||||
filter_search(text.c_str(), &_q_search_res);
|
||||
q_filter->refilter();
|
||||
}
|
||||
|
||||
bool QueueWindow :: filter_ids(const Gtk::TreeIter &iter)
|
||||
{
|
||||
unsigned int id;
|
||||
|
||||
if (_q_search_empty)
|
||||
return true;
|
||||
|
||||
id = q_model->iter_to_id(iter);
|
||||
return set_has(&_q_search_res, queue_at(_queue, id)->tr_dbe.dbe_index);
|
||||
}
|
||||
|
||||
bool QueueWindow :: on_key_press(GdkEventKey *event)
|
||||
{
|
||||
std::string key = gdk_keyval_name(event->keyval);
|
||||
if (key == "space")
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
void QueueWindow :: on_row_activated(const Gtk::TreePath &path,
|
||||
Gtk::TreeViewColumn *col)
|
||||
{
|
||||
Gtk::TreePath real_path = q_filter->convert_path_to_child_path(path);
|
||||
q_model->on_path_selected(real_path);
|
||||
}
|
|
@ -0,0 +1,508 @@
|
|||
/*
|
||||
* Copyright 2015 (c) Anna Schumaker.
|
||||
*/
|
||||
#include <core/settings.h>
|
||||
#include <core/string.h>
|
||||
#include <gui/model.h>
|
||||
#include <gui/sidebar.h>
|
||||
#include <gui/treeview.h>
|
||||
|
||||
enum sidebar_columns {
|
||||
SB_IMAGE,
|
||||
SB_NAME,
|
||||
SB_TYPE,
|
||||
SB_EDITABLE,
|
||||
};
|
||||
|
||||
const gchar *SIDEBAR_SETTING = "gui.sidebar.pos";
|
||||
|
||||
static gchar *__gui_sidebar_size_str(struct playlist *playlist)
|
||||
{
|
||||
const gchar *fmt = "%s\n%d track%s";
|
||||
unsigned int size;
|
||||
|
||||
if (!playlist)
|
||||
return NULL;
|
||||
|
||||
size = playlist_size(playlist);
|
||||
if (playlist_current() == playlist)
|
||||
fmt = "<b>%s\n%d track%s</b>";
|
||||
return g_markup_printf_escaped(fmt, playlist->pl_name, size,
|
||||
(size != 1) ? "s" : "");
|
||||
}
|
||||
|
||||
static void __gui_sidebar_set(GtkTreeIter *iter, const gchar *name,
|
||||
const gchar *image, enum playlist_type_t type)
|
||||
{
|
||||
gtk_tree_store_set(gui_sidebar_store(), iter, SB_NAME, name,
|
||||
SB_IMAGE, image,
|
||||
SB_TYPE, type,
|
||||
SB_EDITABLE, false, -1);
|
||||
}
|
||||
|
||||
static void __gui_sidebar_set_playlist(GtkTreeIter *iter,
|
||||
struct playlist *playlist,
|
||||
const gchar *image)
|
||||
{
|
||||
gchar *text = __gui_sidebar_size_str(playlist);
|
||||
__gui_sidebar_set(iter, text, image, playlist->pl_type);
|
||||
g_free(text);
|
||||
}
|
||||
|
||||
static void __gui_sidebar_add_header(GtkTreeIter *iter, const gchar *name,
|
||||
const gchar *image)
|
||||
{
|
||||
gchar *formatted = g_strdup_printf("<big>%s</big>", name);
|
||||
|
||||
gtk_tree_store_insert(gui_sidebar_store(), iter, NULL, -1);
|
||||
__gui_sidebar_set(iter, NULL, NULL, PL_MAX_TYPE);
|
||||
gtk_tree_store_insert(gui_sidebar_store(), iter, NULL, -1);
|
||||
__gui_sidebar_set(iter, formatted, image, PL_MAX_TYPE);
|
||||
|
||||
g_free(formatted);
|
||||
}
|
||||
|
||||
static int __gui_sidebar_compare(GtkTreeIter *iter, const gchar *name,
|
||||
enum playlist_type_t type)
|
||||
{
|
||||
gchar *cur;
|
||||
int ret;
|
||||
|
||||
if (gui_sidebar_iter_type(iter) != type)
|
||||
return gui_sidebar_iter_type(iter) - type;
|
||||
|
||||
cur = gui_sidebar_iter_name(iter);
|
||||
ret = g_utf8_collate(cur, name);
|
||||
g_free(cur);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static inline void __gui_sidebar_filter_iter_convert(GtkTreeIter *iter,
|
||||
GtkTreeIter *child)
|
||||
{
|
||||
gtk_tree_model_filter_convert_iter_to_child_iter(gui_sidebar_filter(),
|
||||
child, iter);
|
||||
}
|
||||
|
||||
static gchar *__gui_sidebar_filter_iter_name(GtkTreeIter *iter)
|
||||
{
|
||||
GtkTreeIter child;
|
||||
|
||||
__gui_sidebar_filter_iter_convert(iter, &child);
|
||||
return gui_sidebar_iter_name(&child);
|
||||
}
|
||||
|
||||
static gboolean __gui_sidebar_visible_func(GtkTreeModel *model,
|
||||
GtkTreeIter *iter,
|
||||
gpointer data)
|
||||
{
|
||||
enum playlist_type_t type = gui_sidebar_iter_type(iter);
|
||||
|
||||
if (type == PL_SYSTEM || type == PL_ARTIST)
|
||||
return playlist_size(gui_sidebar_iter_playlist(iter)) > 0;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static gboolean __gui_sidebar_can_select(GtkTreeSelection *selection,
|
||||
GtkTreeModel *model, GtkTreePath *path,
|
||||
gboolean selected, gpointer data)
|
||||
{
|
||||
GtkTreeIter iter, child;
|
||||
|
||||
gtk_tree_model_get_iter(model, &iter, path);
|
||||
__gui_sidebar_filter_iter_convert(&iter, &child);
|
||||
return gui_sidebar_iter_type(&child) != PL_MAX_TYPE;
|
||||
}
|
||||
|
||||
void __gui_sidebar_selection_changed(GtkTreeSelection *selection, gpointer data)
|
||||
{
|
||||
bool active = false, sensitive = false;
|
||||
struct playlist *playlist = NULL;
|
||||
GtkTreeIter iter;
|
||||
|
||||
if (gui_sidebar_iter_current(&iter)) {
|
||||
playlist = gui_sidebar_iter_playlist(&iter);
|
||||
active = playlist->pl_random;
|
||||
sensitive = (playlist->pl_ops->pl_set_random != NULL);
|
||||
}
|
||||
|
||||
gui_treeview_set_playlist(playlist);
|
||||
gtk_toggle_button_set_active(gui_random_button(), active);
|
||||
gtk_widget_set_sensitive(GTK_WIDGET(gui_random_button()), sensitive);
|
||||
}
|
||||
|
||||
static void __gui_sidebar_do_rename(GtkTreePath *path)
|
||||
{
|
||||
GtkTreeView *treeview = gui_sidebar_treeview();
|
||||
GtkTreeModel *model = gtk_tree_view_get_model(treeview);
|
||||
GtkTreeViewColumn *column = gtk_tree_view_get_column(treeview, SB_NAME);
|
||||
GtkTreeIter iter, child;
|
||||
|
||||
if (!gtk_tree_model_get_iter(model, &iter, path))
|
||||
return;
|
||||
|
||||
__gui_sidebar_filter_iter_convert(&iter, &child);
|
||||
gui_sidebar_iter_set_editable(&child, true);
|
||||
gtk_tree_view_set_cursor(treeview, path, column, true);
|
||||
}
|
||||
|
||||
static GtkTreePath *__gui_sidebar_current_path(void)
|
||||
{
|
||||
GtkTreeView *treeview = gui_sidebar_treeview();
|
||||
GtkTreeSelection *selection = gtk_tree_view_get_selection(treeview);
|
||||
GtkTreeModel *model = gtk_tree_view_get_model(treeview);
|
||||
GtkTreeIter iter;
|
||||
|
||||
if (!gtk_tree_selection_get_selected(selection, &model, &iter))
|
||||
return NULL;
|
||||
return gtk_tree_model_get_path(model, &iter);
|
||||
}
|
||||
|
||||
bool __gui_sidebar_rename(GtkMenuItem *item, gpointer data)
|
||||
{
|
||||
GtkTreePath *path = __gui_sidebar_current_path();
|
||||
if (path) {
|
||||
__gui_sidebar_do_rename(path);
|
||||
gtk_tree_path_free(path);
|
||||
}
|
||||
return path != NULL;
|
||||
}
|
||||
|
||||
bool __gui_sidebar_select(GtkMenuItem *item, gpointer data)
|
||||
{
|
||||
GtkTreeView *treeview = gui_sidebar_treeview();
|
||||
GtkTreePath *path = __gui_sidebar_current_path();
|
||||
|
||||
if (path) {
|
||||
gtk_tree_view_row_activated(treeview, path, NULL);
|
||||
gtk_tree_path_free(path);
|
||||
}
|
||||
return path != NULL;
|
||||
}
|
||||
|
||||
bool __gui_sidebar_delete(GtkMenuItem *item, gpointer data)
|
||||
{
|
||||
GtkTreeIter iter;
|
||||
|
||||
if (!gui_sidebar_iter_current(&iter))
|
||||
return false;
|
||||
if (playlist_delete(gui_model_get_playlist()))
|
||||
gtk_tree_store_remove(gui_sidebar_store(), &iter);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool __gui_sidebar_keypress(GtkTreeView *treeview, GdkEventKey *event,
|
||||
gpointer data)
|
||||
{
|
||||
switch (event->keyval) {
|
||||
case GDK_KEY_BackSpace:
|
||||
return __gui_sidebar_rename(NULL, NULL);
|
||||
case GDK_KEY_Return:
|
||||
return __gui_sidebar_select(NULL, NULL);
|
||||
case GDK_KEY_Delete:
|
||||
return __gui_sidebar_delete(NULL, NULL);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool __gui_sidebar_button_press(GtkTreeView *treeview, GdkEventButton *event,
|
||||
gpointer data)
|
||||
{
|
||||
enum playlist_type_t type = PL_MAX_TYPE;
|
||||
GtkTreePath *path;
|
||||
GtkTreeIter iter;
|
||||
bool ret = true;
|
||||
|
||||
if (!gtk_tree_view_get_path_at_pos(treeview, event->x, event->y,
|
||||
&path, NULL, NULL, NULL))
|
||||
return false;
|
||||
|
||||
if (event->button == GDK_BUTTON_SECONDARY) {
|
||||
gtk_tree_view_set_cursor(treeview, path, NULL, false);
|
||||
if (gui_sidebar_iter_current(&iter))
|
||||
type = gui_sidebar_iter_type(&iter);
|
||||
gtk_widget_set_visible(gui_builder_widget("rc_sidebar_rename"),
|
||||
type == PL_USER);
|
||||
gtk_menu_popup_at_pointer(gui_sidebar_menu(), (GdkEvent *)event);
|
||||
} else if (event->type == GDK_2BUTTON_PRESS &&
|
||||
event->button == GDK_BUTTON_MIDDLE) {
|
||||
__gui_sidebar_do_rename(path);
|
||||
} else
|
||||
ret = false;
|
||||
|
||||
gtk_tree_path_free(path);
|
||||
return ret;
|
||||
}
|
||||
|
||||
void __gui_sidebar_resized(GtkPaned *pane, GParamSpec *pspec, gpointer data)
|
||||
{
|
||||
settings_set(SIDEBAR_SETTING, gtk_paned_get_position(pane));
|
||||
}
|
||||
|
||||
void __gui_sidebar_random_toggled(GtkToggleButton *button, gpointer data)
|
||||
{
|
||||
struct playlist *playlist = gui_model_get_playlist();
|
||||
bool active = gtk_toggle_button_get_active(button);
|
||||
|
||||
if (playlist)
|
||||
playlist_set_random(playlist, active);
|
||||
}
|
||||
|
||||
void gui_sidebar_init()
|
||||
{
|
||||
int pos = settings_get(SIDEBAR_SETTING);
|
||||
GtkTreeSelection *selection;
|
||||
GtkTreeIter iter;
|
||||
|
||||
gtk_tree_view_enable_model_drag_dest(gui_sidebar_treeview(),
|
||||
gui_model_drag_targets, gui_model_n_targets,
|
||||
GDK_ACTION_MOVE);
|
||||
|
||||
if (!gui_sidebar_iter_first(&iter)) {
|
||||
__gui_sidebar_add_header(&iter, "Playlists", "emblem-documents");
|
||||
__gui_sidebar_add_header(&iter, "Dynamic", "emblem-generic");
|
||||
__gui_sidebar_add_header(&iter, "Library", "emblem-system");
|
||||
|
||||
selection = gtk_tree_view_get_selection(gui_sidebar_treeview());
|
||||
gtk_tree_selection_set_select_function(selection,
|
||||
__gui_sidebar_can_select,
|
||||
NULL, NULL);
|
||||
gtk_tree_model_filter_set_visible_func(gui_sidebar_filter(),
|
||||
__gui_sidebar_visible_func,
|
||||
NULL, NULL);
|
||||
}
|
||||
|
||||
if (pos > 0)
|
||||
gtk_paned_set_position(gui_sidebar(), pos);
|
||||
}
|
||||
|
||||
gboolean gui_sidebar_iter_current(GtkTreeIter *iter)
|
||||
{
|
||||
GtkTreeView *treeview = gui_sidebar_treeview();
|
||||
GtkTreeSelection *selection = gtk_tree_view_get_selection(treeview);
|
||||
GtkTreeModel *model = GTK_TREE_MODEL(gui_sidebar_filter());
|
||||
GtkTreeIter it;
|
||||
|
||||
if (!gtk_tree_selection_get_selected(selection, &model, &it))
|
||||
return false;
|
||||
__gui_sidebar_filter_iter_convert(&it, iter);
|
||||
return true;
|
||||
}
|
||||
|
||||
gboolean gui_sidebar_iter_first(GtkTreeIter *iter)
|
||||
{
|
||||
return gtk_tree_model_get_iter_first(gui_sidebar_model(), iter);
|
||||
}
|
||||
|
||||
gboolean gui_sidebar_iter_next(GtkTreeIter *iter)
|
||||
{
|
||||
return gtk_tree_model_iter_next(gui_sidebar_model(), iter);
|
||||
}
|
||||
|
||||
gboolean gui_sidebar_iter_down(GtkTreeIter *iter, GtkTreeIter *child)
|
||||
{
|
||||
return gtk_tree_model_iter_children(gui_sidebar_model(), child, iter);
|
||||
}
|
||||
|
||||
gchar *gui_sidebar_iter_name(GtkTreeIter *iter)
|
||||
{
|
||||
gchar *text = NULL, *parsed = NULL, *name, **split;
|
||||
|
||||
gtk_tree_model_get(gui_sidebar_model(), iter, SB_NAME, &text, -1);
|
||||
if (!text)
|
||||
return g_strdup("");
|
||||
|
||||
pango_parse_markup(text, -1, 0, NULL, &parsed, NULL, NULL);
|
||||
if (!parsed)
|
||||
return g_strdup("");
|
||||
|
||||
split = g_strsplit(parsed, "\n", 2);
|
||||
name = g_strdup(split[0]);
|
||||
|
||||
g_strfreev(split);
|
||||
g_free(parsed);
|
||||
g_free(text);
|
||||
return name;
|
||||
}
|
||||
|
||||
enum playlist_type_t gui_sidebar_iter_type(GtkTreeIter *iter)
|
||||
{
|
||||
enum playlist_type_t type;
|
||||
gtk_tree_model_get(gui_sidebar_model(), iter, SB_TYPE, &type, -1);
|
||||
return type;
|
||||
}
|
||||
|
||||
bool gui_sidebar_iter_editable(GtkTreeIter *iter)
|
||||
{
|
||||
gboolean editable;
|
||||
gtk_tree_model_get(gui_sidebar_model(), iter, SB_EDITABLE, &editable, -1);
|
||||
return editable == TRUE;
|
||||
}
|
||||
|
||||
struct playlist *gui_sidebar_iter_playlist(GtkTreeIter *iter)
|
||||
{
|
||||
enum playlist_type_t type = gui_sidebar_iter_type(iter);
|
||||
gchar *name = gui_sidebar_iter_name(iter);
|
||||
struct playlist *playlist = playlist_lookup(type, name);
|
||||
|
||||
g_free(name);
|
||||
return playlist;
|
||||
}
|
||||
|
||||
void gui_sidebar_iter_add(GtkTreeIter *iter, struct playlist *playlist,
|
||||
const gchar *image)
|
||||
{
|
||||
GtkTreeIter new;
|
||||
gtk_tree_store_insert_before(gui_sidebar_store(), &new, NULL, iter);
|
||||
__gui_sidebar_set_playlist(&new, playlist, image);
|
||||
}
|
||||
|
||||
void gui_sidebar_iter_sort_child(GtkTreeIter *iter, struct playlist *playlist,
|
||||
const gchar *image)
|
||||
{
|
||||
GtkTreeIter child, new;
|
||||
|
||||
if (!gui_sidebar_iter_down(iter, &child))
|
||||
goto out_append;
|
||||
|
||||
do {
|
||||
if (__gui_sidebar_compare(&child, playlist->pl_name,
|
||||
playlist->pl_type) >= 0) {
|
||||
gtk_tree_store_insert_before(gui_sidebar_store(),
|
||||
&new, iter, &child);
|
||||
__gui_sidebar_set_playlist(&new, playlist, image);
|
||||
return;
|
||||
}
|
||||
} while (gui_sidebar_iter_next(&child));
|
||||
|
||||
out_append:
|
||||
gui_sidebar_iter_append_child(iter, playlist, image);
|
||||
}
|
||||
|
||||
void gui_sidebar_iter_append_child(GtkTreeIter *iter, struct playlist *playlist,
|
||||
const gchar *image)
|
||||
{
|
||||
GtkTreeIter new;
|
||||
gtk_tree_store_insert_before(gui_sidebar_store(), &new, iter, NULL);
|
||||
__gui_sidebar_set_playlist(&new, playlist, image);
|
||||
}
|
||||
|
||||
void gui_sidebar_iter_update_playlist(GtkTreeIter *iter,
|
||||
struct playlist *playlist)
|
||||
{
|
||||
gchar *text;
|
||||
|
||||
if (!playlist)
|
||||
return;
|
||||
|
||||
text = __gui_sidebar_size_str(playlist);
|
||||
gtk_tree_store_set(gui_sidebar_store(), iter, SB_NAME, text, -1);
|
||||
g_free(text);
|
||||
}
|
||||
|
||||
void gui_sidebar_iter_update(GtkTreeIter *iter)
|
||||
{
|
||||
gui_sidebar_iter_update_playlist(iter, gui_sidebar_iter_playlist(iter));
|
||||
}
|
||||
|
||||
void gui_sidebar_iter_select(GtkTreeIter *iter)
|
||||
{
|
||||
GtkTreeSelection *selection;
|
||||
GtkTreeIter filter;
|
||||
|
||||
gtk_tree_model_filter_convert_child_iter_to_iter(gui_sidebar_filter(),
|
||||
&filter, iter);
|
||||
|
||||
selection = gtk_tree_view_get_selection(gui_sidebar_treeview());
|
||||
gtk_tree_selection_select_iter(selection, &filter);
|
||||
}
|
||||
|
||||
bool gui_sidebar_iter_set_editable(GtkTreeIter *iter, bool editable)
|
||||
{
|
||||
enum playlist_type_t type = gui_sidebar_iter_type(iter);
|
||||
if (type != PL_USER)
|
||||
return false;
|
||||
gtk_tree_store_set(gui_sidebar_store(), iter, SB_EDITABLE, editable, -1);
|
||||
return true;
|
||||
}
|
||||
|
||||
void gui_sidebar_filter_path_select(GtkTreePath *path)
|
||||
{
|
||||
GtkTreeModel *model = GTK_TREE_MODEL(gui_sidebar_filter());
|
||||
GtkTreeIter iter, child;
|
||||
|
||||
gtk_tree_model_get_iter(model, &iter, path);
|
||||
__gui_sidebar_filter_iter_convert(&iter, &child);
|
||||
|
||||
if (playlist_select(gui_sidebar_iter_playlist(&child)))
|
||||
gui_sidebar_iter_update(&child);
|
||||
}
|
||||
|
||||
void gui_sidebar_filter_set_expand(GtkTreeIter *iter)
|
||||
{
|
||||
gchar *name = __gui_sidebar_filter_iter_name(iter);
|
||||
gchar *setting = g_strdup_printf("gui.sidebar.expand.%s", name);
|
||||
GtkTreePath *path;
|
||||
|
||||
if (settings_get(setting) == true) {
|
||||
path = gtk_tree_model_get_path(
|
||||
GTK_TREE_MODEL(gui_sidebar_filter()), iter);
|
||||
|
||||
gtk_tree_view_expand_row(gui_sidebar_treeview(), path, false);
|
||||
gtk_tree_path_free(path);
|
||||
}
|
||||
|
||||
g_free(setting);
|
||||
g_free(name);
|
||||
}
|
||||
|
||||
void gui_sidebar_filter_row_expanded(GtkTreeIter *iter, bool expanded)
|
||||
{
|
||||
gchar *name = __gui_sidebar_filter_iter_name(iter);
|
||||
gchar *setting = g_strdup_printf("gui.sidebar.expand.%s", name);
|
||||
|
||||
settings_set(setting, expanded);
|
||||
|
||||
g_free(setting);
|
||||
g_free(name);
|
||||
}
|
||||
|
||||
gboolean gui_sidebar_iter_find(GtkTreeIter *iter, const gchar *name,
|
||||
enum playlist_type_t type)
|
||||
{
|
||||
do {
|
||||
if (__gui_sidebar_compare(iter, name, type) == 0)
|
||||
return TRUE;
|
||||
} while (gui_sidebar_iter_next(iter));
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
gboolean gui_sidebar_iter_from_string(const gchar *path, GtkTreeIter *child)
|
||||
{
|
||||
GtkTreeModel *model = GTK_TREE_MODEL(gui_sidebar_filter());
|
||||
GtkTreeIter iter;
|
||||
|
||||
if (!gtk_tree_model_get_iter_from_string(model, &iter, path))
|
||||
return FALSE;
|
||||
__gui_sidebar_filter_iter_convert(&iter, child);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
gboolean gui_sidebar_iter_from_xy(gint x, gint y, GtkTreeIter *child)
|
||||
{
|
||||
GtkTreeModel *model = GTK_TREE_MODEL(gui_sidebar_filter());
|
||||
GtkTreePath *path;
|
||||
GtkTreeIter iter;
|
||||
|
||||
if (!gtk_tree_view_get_path_at_pos(gui_sidebar_treeview(), x, y,
|
||||
&path, NULL, NULL, NULL))
|
||||
return false;
|
||||
|
||||
gtk_tree_model_get_iter(model, &iter, path);
|
||||
__gui_sidebar_filter_iter_convert(&iter, child);
|
||||
gtk_tree_path_free(path);
|
||||
return true;
|
||||
}
|
471
gui/tabs.cpp
471
gui/tabs.cpp
|
@ -1,471 +0,0 @@
|
|||
/*
|
||||
* Copyright 2014 (c) Anna Schumaker.
|
||||
*/
|
||||
#include <core/deck.h>
|
||||
#include <core/playlist.h>
|
||||
extern "C" {
|
||||
#include <core/string.h>
|
||||
}
|
||||
#include <gui/tabs.h>
|
||||
|
||||
#include <map>
|
||||
#include <sstream>
|
||||
|
||||
|
||||
static std::map<queue *, Tab *> queue_mapping;
|
||||
|
||||
static compare_t sort_fields[] = {
|
||||
COMPARE_TRACK, COMPARE_TITLE, COMPARE_LENGTH,
|
||||
COMPARE_ARTIST, COMPARE_ALBUM, COMPARE_YEAR,
|
||||
COMPARE_GENRE, COMPARE_COUNT, COMPARE_PLAYED
|
||||
};
|
||||
|
||||
static void tempq_added(struct queue *queue, unsigned int pos)
|
||||
{
|
||||
find_tab(queue)->on_track_added(pos);
|
||||
deck :: write();
|
||||
}
|
||||
|
||||
static void tempq_removed(struct queue *queue, unsigned int pos)
|
||||
{
|
||||
find_tab(queue)->on_track_removed(pos);
|
||||
deck :: write();
|
||||
}
|
||||
|
||||
static void tempq_cleared(struct queue *queue, unsigned int n)
|
||||
{
|
||||
find_tab(queue)->on_tracks_cleared(n);
|
||||
deck :: write();
|
||||
}
|
||||
|
||||
static void tempq_updated(struct queue *queue, unsigned int pos)
|
||||
{
|
||||
find_tab(queue)->on_track_updated(pos);
|
||||
}
|
||||
|
||||
struct queue_ops tempq_ops = {
|
||||
tempq_added,
|
||||
tempq_removed,
|
||||
tempq_cleared,
|
||||
deck :: save,
|
||||
tempq_updated,
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* Tab class basics
|
||||
*
|
||||
*/
|
||||
|
||||
Tab :: Tab(queue *pq)
|
||||
: tab_sorting_count(0), tab_pq(pq), tab_label(NULL)
|
||||
{
|
||||
queue_mapping[tab_pq] = this;
|
||||
|
||||
tab_builder = Gtk::Builder::create();
|
||||
tab_builder->add_from_file(gui :: share_file("QueueToolbar.ui"));
|
||||
tab_builder->add_from_file(gui :: share_file("QueueWindow.ui"));
|
||||
tab_builder->get_widget_derived("QueueToolbar", tab_toolbar);
|
||||
tab_builder->get_widget_derived("QueueWindow", tab_window);
|
||||
|
||||
tab_window->init(tab_pq);
|
||||
tab_window->q_treeview->signal_key_press_event().connect(sigc::mem_fun(*this,
|
||||
&Tab :: on_key_pressed));
|
||||
tab_window->q_treeview->signal_button_press_event().connect(sigc::mem_fun(*this,
|
||||
&Tab :: on_button_pressed), false);
|
||||
|
||||
for (unsigned int i = 0; i < tab_window->q_treeview->get_n_columns(); i++)
|
||||
tab_window->q_treeview->get_column(i)->signal_clicked().connect(
|
||||
sigc::bind<unsigned int> (sigc::mem_fun(
|
||||
*this, &Tab::on_column_clicked), i));
|
||||
|
||||
tab_vbox.set_margin_start(1);
|
||||
tab_vbox.set_margin_end(1);
|
||||
tab_vbox.set_homogeneous(false);
|
||||
|
||||
tab_vbox.pack_start(*tab_toolbar, false, true, 2);
|
||||
tab_vbox.show();
|
||||
|
||||
tab_runtime_changed();
|
||||
}
|
||||
|
||||
Tab :: ~Tab() {}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* QNotifier implementation.
|
||||
*
|
||||
*/
|
||||
|
||||
void Tab :: on_track_added(unsigned int row)
|
||||
{
|
||||
tab_window->q_model->on_row_inserted(row);
|
||||
tab_label->set_size();
|
||||
tab_runtime_changed();
|
||||
}
|
||||
|
||||
void Tab :: on_track_removed(unsigned int row)
|
||||
{
|
||||
tab_window->q_model->on_row_deleted(row);
|
||||
tab_label->set_size();
|
||||
tab_runtime_changed();
|
||||
}
|
||||
|
||||
void Tab :: on_tracks_cleared(unsigned int n)
|
||||
{
|
||||
tab_window->q_model->on_cleared(n);
|
||||
tab_label->set_size();
|
||||
tab_runtime_changed();
|
||||
}
|
||||
|
||||
void Tab :: on_track_updated(unsigned int row)
|
||||
{
|
||||
tab_window->q_model->on_row_changed(row);
|
||||
tab_runtime_changed();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* Tab internal helper functions
|
||||
*
|
||||
*/
|
||||
|
||||
int Tab :: tab_page_num()
|
||||
{
|
||||
Gtk::Notebook *notebook = gui :: get_widget<Gtk::Notebook>("o_notebook");
|
||||
return notebook->page_num(tab_vbox);
|
||||
}
|
||||
|
||||
bool Tab :: tab_is_cur()
|
||||
{
|
||||
Gtk::Notebook *notebook = gui :: get_widget<Gtk::Notebook>("o_notebook");
|
||||
return notebook->page_num(tab_vbox) == notebook->get_current_page();
|
||||
}
|
||||
|
||||
void Tab :: tab_runtime_changed()
|
||||
{
|
||||
gchar *len = string_sec2str_long(tab_pq->q_length);
|
||||
if (tab_is_cur())
|
||||
gui :: get_widget<Gtk::Label>("o_queue_time")->set_text(len);
|
||||
g_free(len);
|
||||
}
|
||||
|
||||
void Tab :: tab_display_sorting()
|
||||
{
|
||||
std::string text = "";
|
||||
if ((tab_sorting_count > 0) && tab_is_cur())
|
||||
text = "Sorting within " + tab_sorting_title;
|
||||
gui :: get_widget<Gtk::Label>("o_sorting_indicator")->set_text(text);
|
||||
}
|
||||
|
||||
void Tab :: tab_dec_sort_count()
|
||||
{
|
||||
tab_sorting_count--;
|
||||
tab_display_sorting();
|
||||
}
|
||||
|
||||
void Tab :: tab_unmap()
|
||||
{
|
||||
queue_mapping.erase(tab_pq);
|
||||
}
|
||||
|
||||
void Tab :: tab_focus_search()
|
||||
{
|
||||
tab_toolbar->q_search->grab_focus();
|
||||
}
|
||||
|
||||
void Tab :: tab_selected_ids(std::vector<unsigned int> &ids)
|
||||
{
|
||||
Glib::RefPtr<Gtk::TreeSelection> sel = tab_window->q_treeview->get_selection();
|
||||
std::vector<Gtk::TreeModel::Path> rows = sel->get_selected_rows();
|
||||
Gtk::TreeModel::Path path;
|
||||
|
||||
for (unsigned int i = 0; i < rows.size(); i++) {
|
||||
path = tab_window->q_filter->convert_path_to_child_path(rows[i]);
|
||||
ids.push_back(tab_window->q_model->path_to_id(path));
|
||||
}
|
||||
sel->unselect_all();
|
||||
}
|
||||
|
||||
void Tab :: tab_queue_add(queue *pq)
|
||||
{
|
||||
std::vector<unsigned int> ids;
|
||||
|
||||
tab_selected_ids(ids);
|
||||
for (unsigned int i = 0; i < ids.size(); i++)
|
||||
queue_add(pq, track_get(ids[i]));
|
||||
}
|
||||
|
||||
bool Tab :: tab_queue_selected(bool random)
|
||||
{
|
||||
if (deck :: get_queues().size() >= 10)
|
||||
return true;
|
||||
|
||||
queue *pq = deck :: create(random, &tempq_ops);
|
||||
on_pq_created(pq, deck :: get_queues().size() - 1);
|
||||
tab_queue_add(pq);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Tab :: tab_add_to_queue(unsigned int n)
|
||||
{
|
||||
if (n >= deck :: get_queues().size())
|
||||
return true;
|
||||
|
||||
queue *pq = deck :: get(n);
|
||||
tab_queue_add(pq);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Tab :: tab_add_to_playlist(const std::string &playlist)
|
||||
{
|
||||
std::vector<unsigned int> ids;
|
||||
|
||||
tab_selected_ids(ids);
|
||||
for (unsigned int i = 0; i < ids.size(); i++)
|
||||
playlist :: add(track_get(ids[i]), playlist);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Tab :: tab_favorite_selected()
|
||||
{
|
||||
return tab_add_to_playlist("Favorites");
|
||||
}
|
||||
|
||||
void Tab :: tab_ban_selected()
|
||||
{
|
||||
tab_add_to_playlist("Banned");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* Tab callback functions
|
||||
*
|
||||
*/
|
||||
|
||||
bool Tab :: on_key_press_event(const std::string &key)
|
||||
{
|
||||
if (key >= "0" && key <= "9")
|
||||
return tab_add_to_queue(atoi(key.c_str()));
|
||||
else if (key == "f")
|
||||
return tab_favorite_selected();
|
||||
else if (key == "q" || key == "r")
|
||||
return tab_queue_selected(key == "r");
|
||||
else
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void Tab :: on_show_rc_menu()
|
||||
{
|
||||
std::string item;
|
||||
unsigned int size = deck :: get_queues().size();
|
||||
|
||||
if (size == 0) {
|
||||
gui :: get_widget<Gtk::MenuItem>("o_add_to_pq")->hide();
|
||||
return;
|
||||
}
|
||||
gui :: get_widget<Gtk::MenuItem>("o_add_to_pq")->show();
|
||||
|
||||
if (size == 10)
|
||||
gui :: get_widget<Gtk::MenuItem>("o_new_pq")->hide();
|
||||
else
|
||||
gui :: get_widget<Gtk::MenuItem>("o_new_pq")->show();
|
||||
|
||||
for (unsigned int i = 0; i < 10; i++) {
|
||||
item = "o_pq_";
|
||||
item += '0' + i;
|
||||
if (i < size)
|
||||
gui :: get_widget<Gtk::MenuItem>(item)->show();
|
||||
else
|
||||
gui :: get_widget<Gtk::MenuItem>(item)->hide();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* GTK-MM callback functions
|
||||
*
|
||||
*/
|
||||
|
||||
bool Tab :: on_key_pressed(GdkEventKey *event)
|
||||
{
|
||||
std::string key = gdk_keyval_name(event->keyval);
|
||||
|
||||
if (key.size() >= 3) {
|
||||
if (key.substr(0, 3) == "KP_")
|
||||
key = key.substr(3);
|
||||
}
|
||||
|
||||
return on_key_press_event(key);
|
||||
}
|
||||
|
||||
void Tab :: on_column_clicked(unsigned int col)
|
||||
{
|
||||
if (tab_sorting_count == 0) {
|
||||
tab_sorting_title = tab_window->q_treeview->get_column(col)->get_title();
|
||||
queue_sort(tab_pq, sort_fields[col], true);
|
||||
} else
|
||||
queue_sort(tab_pq, sort_fields[col], false);
|
||||
|
||||
tab_sorting_count++;
|
||||
tab_display_sorting();
|
||||
Glib::signal_timeout().connect_seconds_once(
|
||||
sigc::mem_fun(*this, &Tab::tab_dec_sort_count), 2);
|
||||
}
|
||||
|
||||
bool Tab :: on_button_pressed(GdkEventButton *button)
|
||||
{
|
||||
if (button->button != 3)
|
||||
return false;
|
||||
|
||||
Gtk::TreeModel::Path path;
|
||||
if (tab_window->q_treeview->get_path_at_pos(button->x, button->y, path))
|
||||
tab_window->q_treeview->get_selection()->select(path);
|
||||
|
||||
on_show_rc_menu();
|
||||
gui :: get_widget<Gtk::Menu>("o_rc_menu")->popup(button->button, button->time);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* Global functions
|
||||
*
|
||||
*/
|
||||
|
||||
Tab *find_tab(queue *pq)
|
||||
{
|
||||
std::map<queue *, Tab *>::iterator it;
|
||||
it = queue_mapping.find(pq);
|
||||
if (it != queue_mapping.end())
|
||||
return it->second;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static Tab *find_tab(int num)
|
||||
{
|
||||
std::map<queue *, Tab *>::iterator it;
|
||||
for (it = queue_mapping.begin(); it != queue_mapping.end(); it++) {
|
||||
if (it->second->tab_page_num() == num)
|
||||
return it->second;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static Tab *cur_tab()
|
||||
{
|
||||
std::map<queue *, Tab *>::iterator it;
|
||||
for (it = queue_mapping.begin(); it != queue_mapping.end(); it++) {
|
||||
if (it->second->tab_is_cur())
|
||||
return it->second;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void on_switch_page(Gtk::Widget *page, int num)
|
||||
{
|
||||
Tab *tab = find_tab(num);
|
||||
if (tab) {
|
||||
tab->tab_runtime_changed();
|
||||
tab->tab_display_sorting();
|
||||
} else
|
||||
gui :: get_widget<Gtk::Label>("o_queue_time")->set_text("");
|
||||
}
|
||||
|
||||
void tab_focus_search()
|
||||
{
|
||||
int page = gui :: get_widget<Gtk::Notebook>("o_notebook")->get_current_page();
|
||||
Tab *tab = find_tab(page);
|
||||
if (tab)
|
||||
tab->tab_focus_search();
|
||||
}
|
||||
|
||||
static void on_new_pq()
|
||||
{
|
||||
Tab *tab = cur_tab();
|
||||
if (tab)
|
||||
tab->tab_queue_selected(false);
|
||||
}
|
||||
|
||||
static void on_add_to_favs()
|
||||
{
|
||||
Tab *tab = cur_tab();
|
||||
if (tab)
|
||||
tab->tab_favorite_selected();
|
||||
}
|
||||
|
||||
static void on_add_to_banned()
|
||||
{
|
||||
Tab *tab = cur_tab();
|
||||
if (tab)
|
||||
tab->tab_ban_selected();
|
||||
}
|
||||
|
||||
static void on_add_to_queue(unsigned int n)
|
||||
{
|
||||
Tab *tab = cur_tab();
|
||||
if (tab)
|
||||
tab->tab_add_to_queue(n);
|
||||
}
|
||||
|
||||
static void init_menu_item(const std::string &pq, unsigned int n)
|
||||
|
||||
{
|
||||
gui :: get_widget<Gtk::MenuItem>(pq)->signal_activate().connect(
|
||||
sigc::bind<unsigned int> (sigc::ptr_fun(on_add_to_queue), n));
|
||||
}
|
||||
|
||||
void init_tabs()
|
||||
{
|
||||
/* Notebook signals */
|
||||
struct Gtk::Notebook *notebook = gui :: get_widget<Gtk::Notebook>("o_notebook");
|
||||
notebook->signal_switch_page().connect(sigc::ptr_fun(on_switch_page));
|
||||
|
||||
/* Menu signals */
|
||||
gui :: get_widget<Gtk::Menu>("o_rc_menu")->show_all();
|
||||
gui :: get_widget<Gtk::MenuItem>("o_new_pq")->signal_activate().connect(
|
||||
sigc::ptr_fun(on_new_pq));
|
||||
gui :: get_widget<Gtk::MenuItem>("o_add_to_favorites")->signal_activate().connect(
|
||||
sigc::ptr_fun(on_add_to_favs));
|
||||
gui :: get_widget<Gtk::MenuItem>("o_add_to_banned")->signal_activate().connect(
|
||||
sigc::ptr_fun(on_add_to_banned));
|
||||
for (unsigned int i = 0; i < 10; i++) {
|
||||
std::string pq = "o_pq_";
|
||||
pq += '0' + i;
|
||||
init_menu_item(pq, i);
|
||||
}
|
||||
|
||||
/* Initialize other tabs */
|
||||
init_history_tab();
|
||||
init_collection_tab();
|
||||
init_queue_tabs();
|
||||
}
|
||||
|
||||
void post_init_tabs()
|
||||
{
|
||||
post_init_queue_tabs();
|
||||
|
||||
unsigned int tab = 0;
|
||||
for (tab = 0; tab < deck::get_queues().size(); tab++) {
|
||||
if (queue_has_flag(deck :: get(tab), Q_ENABLED))
|
||||
break;
|
||||
}
|
||||
gui :: get_widget<Gtk::Notebook>("o_notebook")->set_current_page(tab);
|
||||
}
|
||||
|
||||
void cleanup_tabs()
|
||||
{
|
||||
while (queue_mapping.size() > 0)
|
||||
delete queue_mapping.begin()->second;
|
||||
}
|
|
@ -0,0 +1,289 @@
|
|||
/*
|
||||
* Copyright 2016 (c) Anna Schumaker.
|
||||
*/
|
||||
#include <core/settings.h>
|
||||
#include <gui/filter.h>
|
||||
#include <gui/model.h>
|
||||
#include <gui/treeview.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
struct col_map_entry {
|
||||
enum compare_t compare;
|
||||
const gchar *setting;
|
||||
};
|
||||
|
||||
static const struct col_map_entry GUI_COL_MAP[GUI_MODEL_N_COLUMNS] = {
|
||||
[GUI_MODEL_TRACK_NR] = { COMPARE_TRACK, "gui.queue.track" },
|
||||
[GUI_MODEL_TITLE] = { COMPARE_TITLE, "gui.queue.title" },
|
||||
[GUI_MODEL_LENGTH] = { COMPARE_LENGTH, "gui.queue.length" },
|
||||
[GUI_MODEL_ARTIST] = { COMPARE_ARTIST, "gui.queue.artist" },
|
||||
[GUI_MODEL_ALBUM] = { COMPARE_ALBUM, "gui.queue.album" },
|
||||
[GUI_MODEL_YEAR] = { COMPARE_YEAR, "gui.queue.year" },
|
||||
[GUI_MODEL_GENRE] = { COMPARE_GENRE, "gui.queue.genre" },
|
||||
[GUI_MODEL_COUNT] = { COMPARE_COUNT, "gui.queue.count" },
|
||||
[GUI_MODEL_LAST_PLAY] = { COMPARE_PLAYED, NULL },
|
||||
};
|
||||
|
||||
static unsigned int sort_count = 0;
|
||||
static gchar *sort_text = NULL;
|
||||
static bool can_scroll = true;
|
||||
|
||||
static int __gui_treeview_colum_match_sort(enum compare_t compare)
|
||||
{
|
||||
struct playlist *playlist = gui_model_get_playlist();
|
||||
GSList *cur = playlist ? playlist->pl_sort : NULL;
|
||||
|
||||
while (cur) {
|
||||
int field = GPOINTER_TO_INT(cur->data);
|
||||
if (abs(field) == compare)
|
||||
return field;
|
||||
cur = g_slist_next(cur);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void __gui_treeview_set_sort_indicators()
|
||||
{
|
||||
GtkTreeViewColumn *col;
|
||||
unsigned int i, order;
|
||||
int field;
|
||||
|
||||
for (i = 0; i < GUI_MODEL_N_COLUMNS; i++) {
|
||||
col = gtk_tree_view_get_column(gui_treeview(), i);
|
||||
if (!col)
|
||||
continue;
|
||||
|
||||
field = __gui_treeview_colum_match_sort(GUI_COL_MAP[i].compare);
|
||||
order = (field > 0) ? GTK_SORT_ASCENDING : GTK_SORT_DESCENDING;
|
||||
|
||||
gtk_tree_view_column_set_sort_indicator(col, field != 0);
|
||||
gtk_tree_view_column_set_sort_order(col, order);
|
||||
}
|
||||
}
|
||||
|
||||
static void __gui_treeview_clear_sorting()
|
||||
{
|
||||
sort_count = 0;
|
||||
|
||||
if (sort_text) {
|
||||
g_free(sort_text);
|
||||
sort_text = NULL;
|
||||
gtk_label_set_text(gui_sorting(), "");
|
||||
}
|
||||
}
|
||||
|
||||
static void __gui_treeview_set_sorting(gchar *text)
|
||||
{
|
||||
gchar *formatted;
|
||||
|
||||
__gui_treeview_clear_sorting();
|
||||
if (!text)
|
||||
return;
|
||||
|
||||
sort_text = g_strdup(text);
|
||||
formatted = g_strdup_printf("Sorting: {%s}", text);
|
||||
gtk_label_set_text(gui_sorting(), formatted);
|
||||
g_free(formatted);
|
||||
}
|
||||
|
||||
static int __gui_treeview_dec_sort(gpointer data)
|
||||
{
|
||||
if (sort_count > 0)
|
||||
sort_count--;
|
||||
if (sort_count == 0)
|
||||
__gui_treeview_clear_sorting();
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static gchar *__gui_treeview_sort_text_append(GtkTreeViewColumn *col)
|
||||
{
|
||||
const gchar *title = gtk_tree_view_column_get_title(col);
|
||||
gchar *text, **split;
|
||||
unsigned int i;
|
||||
|
||||
if (!sort_text)
|
||||
return g_strdup(title);
|
||||
if (gtk_tree_view_column_get_sort_order(col) == GTK_SORT_ASCENDING)
|
||||
return g_strdup_printf("%s, %s", sort_text, title);
|
||||
|
||||
/* Find the column and prefix it with a minus sign */
|
||||
split = g_strsplit(sort_text, ", ", 0);
|
||||
for (i = 0; split[i] != NULL; i++) {
|
||||
if (g_strcmp0(split[i], title) == 0)
|
||||
break;
|
||||
}
|
||||
|
||||
g_free(split[i]);
|
||||
split[i] = g_strdup_printf("-%s", title);
|
||||
text = g_strjoinv(", ", split);
|
||||
g_strfreev(split);
|
||||
return text;
|
||||
}
|
||||
|
||||
static void __gui_treeview_column_clicked(GtkTreeViewColumn *col,
|
||||
gpointer data)
|
||||
{
|
||||
struct playlist *playlist = gui_model_get_playlist();
|
||||
enum compare_t compare = GPOINTER_TO_UINT(data);
|
||||
gchar *text;
|
||||
|
||||
if (!playlist)
|
||||
return;
|
||||
|
||||
if (sort_count == 0)
|
||||
playlist_clear_sort(playlist);
|
||||
if (!playlist_sort(playlist, compare))
|
||||
return;
|
||||
__gui_treeview_set_sort_indicators();
|
||||
|
||||
text = __gui_treeview_sort_text_append(col);
|
||||
__gui_treeview_set_sorting(text);
|
||||
g_free(text);
|
||||
|
||||
sort_count++;
|
||||
g_timeout_add_seconds(3, __gui_treeview_dec_sort, NULL);
|
||||
}
|
||||
|
||||
void __gui_treeview_column_resized(GtkTreeViewColumn *col, GParamSpec *pspec,
|
||||
gpointer data)
|
||||
{
|
||||
settings_set(GUI_COL_MAP[GPOINTER_TO_UINT(data)].setting,
|
||||
gtk_tree_view_column_get_width(col));
|
||||
}
|
||||
|
||||
void __gui_treeview_row_activated(GtkTreeView *treeview, GtkTreePath *path,
|
||||
GtkTreeViewColumn *col, gpointer data)
|
||||
{
|
||||
can_scroll = false;
|
||||
gui_filter_path_load_track(path);
|
||||
can_scroll = true;
|
||||
}
|
||||
|
||||
void __gui_treeview_drag_data_received(GtkTreeView *treeview, GdkDragContext *context,
|
||||
gint x, gint y, GtkSelectionData *data,
|
||||
guint info, guint time, gpointer user_data)
|
||||
{
|
||||
struct gui_model_drag_data *drag_data;
|
||||
unsigned int to, from;
|
||||
GtkTreePath *path;
|
||||
|
||||
drag_data = (void *)gtk_selection_data_get_data(data);
|
||||
if (gtk_tree_view_get_path_at_pos(gui_treeview(), x, y,
|
||||
&path, NULL, NULL, NULL))
|
||||
gtk_tree_path_prev(path);
|
||||
else if (!gtk_tree_view_get_visible_range(gui_treeview(), NULL, &path))
|
||||
return;
|
||||
|
||||
from = drag_data->drag_row;
|
||||
to = gui_filter_path_get_index(path);
|
||||
|
||||
if (playlist_rearrange(gui_model_get_playlist(), from, to)) {
|
||||
gtk_tree_selection_unselect_all(gui_treeview_selection());
|
||||
gtk_tree_selection_select_path(gui_treeview_selection(), path);
|
||||
__gui_treeview_set_sort_indicators();
|
||||
}
|
||||
|
||||
g_signal_stop_emission_by_name(treeview, "drag_data_received");
|
||||
gtk_drag_finish(context, true, true, time);
|
||||
gtk_tree_path_free(path);
|
||||
}
|
||||
|
||||
bool __gui_treeview_drag_drop(GtkTreeView *treeview, GdkDragContext *context,
|
||||
gint x, gint y, guint time, gpointer user_data)
|
||||
{
|
||||
gtk_drag_get_data(GTK_WIDGET(treeview), context,
|
||||
gdk_atom_intern(GUI_DRAG_DATA, false), time);
|
||||
return true;
|
||||
}
|
||||
|
||||
void gui_treeview_init()
|
||||
{
|
||||
GtkTreeViewColumn *col;
|
||||
int i, pos;
|
||||
|
||||
gtk_tree_view_set_model(gui_treeview(),
|
||||
GTK_TREE_MODEL(gui_filter_get()));
|
||||
gtk_tree_view_enable_model_drag_source(gui_treeview(), GDK_BUTTON1_MASK,
|
||||
gui_model_drag_targets, gui_model_n_targets,
|
||||
GDK_ACTION_MOVE);
|
||||
gtk_tree_view_enable_model_drag_dest(gui_treeview(),
|
||||
gui_model_drag_targets, gui_model_n_targets,
|
||||
GDK_ACTION_MOVE);
|
||||
|
||||
for (i = 0; i < GUI_MODEL_N_COLUMNS; i++) {
|
||||
col = gtk_tree_view_get_column(gui_treeview(), i);
|
||||
if (col) {
|
||||
g_signal_connect(col, "clicked",
|
||||
G_CALLBACK(__gui_treeview_column_clicked),
|
||||
GUINT_TO_POINTER(GUI_COL_MAP[i].compare));
|
||||
g_signal_connect(col, "notify::width",
|
||||
G_CALLBACK(__gui_treeview_column_resized),
|
||||
GUINT_TO_POINTER(i));
|
||||
pos = settings_get(GUI_COL_MAP[i].setting);
|
||||
if (pos > 0)
|
||||
gtk_tree_view_column_set_fixed_width(col, pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void gui_treeview_deinit()
|
||||
{
|
||||
__gui_treeview_clear_sorting();
|
||||
}
|
||||
|
||||
void gui_treeview_set_playlist(struct playlist *playlist)
|
||||
{
|
||||
gui_filter_set_playlist(playlist);
|
||||
|
||||
__gui_treeview_clear_sorting();
|
||||
__gui_treeview_set_sort_indicators();
|
||||
|
||||
gui_treeview_scroll();
|
||||
}
|
||||
|
||||
void gui_treeview_scroll()
|
||||
{
|
||||
int pos = playlist_current_index(gui_model_get_playlist());
|
||||
GtkTreePath *path;
|
||||
|
||||
if (!can_scroll || pos < 0)
|
||||
return;
|
||||
|
||||
path = gui_filter_path_from_index(pos);
|
||||
if (!path)
|
||||
return;
|
||||
|
||||
gtk_tree_view_set_cursor(gui_treeview(), path, NULL, false);
|
||||
gtk_tree_view_scroll_to_cell(gui_treeview(), path, NULL, true, 0.5, 0.5);
|
||||
gtk_tree_path_free(path);
|
||||
}
|
||||
|
||||
void gui_treeview_select_path_at_pos(unsigned int x, unsigned int y)
|
||||
{
|
||||
GtkTreePath *path;
|
||||
|
||||
if (gtk_tree_view_get_path_at_pos(gui_treeview(), x, y,
|
||||
&path, NULL, NULL, NULL))
|
||||
{
|
||||
gtk_tree_selection_select_path(gui_treeview_selection(), path);
|
||||
gtk_tree_path_free(path);
|
||||
}
|
||||
}
|
||||
|
||||
GList *gui_treeview_list_selected_tracks(void)
|
||||
{
|
||||
GtkTreeSelection *selection = gui_treeview_selection();
|
||||
GList *rows = gtk_tree_selection_get_selected_rows(selection, NULL);
|
||||
GList *cur = g_list_first(rows);
|
||||
GList *list = NULL;
|
||||
|
||||
while (cur) {
|
||||
list = g_list_append(list, gui_filter_path_get_track(cur->data));
|
||||
cur = g_list_next(cur);
|
||||
}
|
||||
|
||||
g_list_free_full(rows, (GDestroyNotify) gtk_tree_path_free);
|
||||
gtk_tree_selection_unselect_all(selection);
|
||||
return list;
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* Copyright 2014 (c) Anna Schumaker.
|
||||
*/
|
||||
#include <core/settings.h>
|
||||
#include <core/version.h>
|
||||
#include <gui/window.h>
|
||||
|
||||
static const gchar *SETTINGS_WIDTH = "gui.window.width";
|
||||
static const gchar *SETTINGS_HEIGHT = "gui.window.height";
|
||||
static const gchar *SETTINGS_X = "gui.window.x";
|
||||
static const gchar *SETTINGS_Y = "gui.window.y";
|
||||
|
||||
static int saved_width = 0;
|
||||
static int saved_height = 0;
|
||||
static int saved_x = 0;
|
||||
static int saved_y = 0;
|
||||
|
||||
|
||||
gboolean __window_configure(GtkWindow *window, GdkEventConfigure *event,
|
||||
gpointer data)
|
||||
{
|
||||
int width = settings_get(SETTINGS_WIDTH);
|
||||
int height = settings_get(SETTINGS_HEIGHT);
|
||||
int x = settings_get(SETTINGS_X);
|
||||
int y = settings_get(SETTINGS_Y);
|
||||
|
||||
if (event->width != width) {
|
||||
saved_width = width;
|
||||
settings_set(SETTINGS_WIDTH, event->width);
|
||||
}
|
||||
|
||||
if (event->height != height) {
|
||||
saved_height = height;
|
||||
settings_set(SETTINGS_HEIGHT, event->height);
|
||||
}
|
||||
|
||||
if (event->x != x) {
|
||||
saved_x = x;
|
||||
settings_set(SETTINGS_X, event->x);
|
||||
}
|
||||
|
||||
if (event->y != y) {
|
||||
saved_y = y;
|
||||
settings_set(SETTINGS_Y, event->y);
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
gboolean __window_state(GtkWindow *window, GdkEventWindowState *event,
|
||||
gpointer data)
|
||||
{
|
||||
if (event->changed_mask & GDK_WINDOW_STATE_MAXIMIZED &&
|
||||
event->new_window_state & GDK_WINDOW_STATE_MAXIMIZED) {
|
||||
settings_set(SETTINGS_WIDTH, saved_width);
|
||||
settings_set(SETTINGS_HEIGHT, saved_height);
|
||||
settings_set(SETTINGS_X, saved_x);
|
||||
settings_set(SETTINGS_Y, saved_y);
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
|
||||
void gui_window_init(const gchar *icon)
|
||||
{
|
||||
gchar *title = g_strdup_printf("Ocarina %s", get_version());
|
||||
|
||||
gtk_window_set_title(gui_window(), title);
|
||||
gtk_window_set_icon_from_file(gui_window(), icon, NULL);
|
||||
|
||||
saved_width = settings_get(SETTINGS_WIDTH);
|
||||
saved_height = settings_get(SETTINGS_HEIGHT);
|
||||
saved_x = settings_get(SETTINGS_X);
|
||||
saved_y = settings_get(SETTINGS_Y);
|
||||
|
||||
if (saved_width > 0 || saved_height > 0)
|
||||
gtk_window_resize(gui_window(), saved_width, saved_height);
|
||||
if (saved_x > 0 || saved_y > 0)
|
||||
gtk_window_move(gui_window(), saved_x, saved_y);
|
||||
|
||||
g_free(title);
|
||||
}
|
||||
|
||||
void gui_window_deinit()
|
||||
{
|
||||
gtk_widget_destroy(GTK_WIDGET(gui_window()));
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
/*
|
||||
* Copyright 2014 (c) Anna Schumaker.
|
||||
*/
|
||||
#include <core/audio.h>
|
||||
#include <core/deck.h>
|
||||
#include <core/version.h>
|
||||
#include <gui/ocarina.h>
|
||||
#include <gui/tabs.h>
|
||||
|
||||
const std::string appname = "Ocarina ";
|
||||
|
||||
static bool on_window_key_pressed(GdkEventKey *event)
|
||||
{
|
||||
Gtk::Notebook *notebook = gui :: get_widget<Gtk::Notebook>("o_notebook");
|
||||
Gtk::Window *window = gui :: get_widget<Gtk::Window>("o_window");
|
||||
std::string key = gdk_keyval_name(event->keyval);
|
||||
|
||||
if (key.size() >= 3) {
|
||||
if (key.substr(0, 3) == "KP_")
|
||||
key = key.substr(3);
|
||||
}
|
||||
|
||||
if (key == "Escape")
|
||||
window->set_focus(*window);
|
||||
else if (key == "slash")
|
||||
tab_focus_search();
|
||||
else if (key >= "0" && key <= "9") {
|
||||
unsigned int n = atoi(key.c_str());
|
||||
if (n < deck::get_queues().size())
|
||||
notebook->set_current_page(n);
|
||||
} else if (key == "c")
|
||||
notebook->set_current_page(deck::get_queues().size());
|
||||
else if (key == "h")
|
||||
notebook->set_current_page(deck::get_queues().size() + 1);
|
||||
else if (key == "m")
|
||||
notebook->set_current_page(deck::get_queues().size() + 3);
|
||||
else if (key == "n")
|
||||
gst :: next();
|
||||
else if (key == "N")
|
||||
audio :: prev();
|
||||
else if (key == "p")
|
||||
notebook->set_current_page(deck::get_queues().size() + 2);
|
||||
else
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool on_window_key_released(GdkEventKey *event)
|
||||
{
|
||||
std::string key = gdk_keyval_name(event->keyval);
|
||||
|
||||
if (key != "space")
|
||||
return false;
|
||||
|
||||
gst :: toggle();
|
||||
return true;
|
||||
}
|
||||
|
||||
Gtk::Window *window_init()
|
||||
{
|
||||
Gtk::Window *window = gui :: get_widget<Gtk::Window>("o_window");
|
||||
|
||||
window->set_can_focus();
|
||||
window->set_title(appname + get_version());
|
||||
window->set_icon_from_file(gui :: share_file("ocarina.png"));
|
||||
|
||||
window->signal_key_press_event().connect(sigc::ptr_fun(on_window_key_pressed));
|
||||
window->signal_key_release_event().connect(sigc::ptr_fun(on_window_key_released));
|
||||
|
||||
return window;
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
import subprocess
|
||||
Import("env")
|
||||
|
||||
version = str(env.Version)
|
||||
if env.Debug == True:
|
||||
version += "-debug"
|
||||
|
||||
try:
|
||||
devnull = open("/dev/null", "w")
|
||||
diff = subprocess.check_output("git diff $(git describe --abbrev=0)",
|
||||
stderr=devnull, shell=True)
|
||||
if diff != "":
|
||||
version += "+"
|
||||
except:
|
||||
pass
|
||||
|
||||
env.Append( CCFLAGS = [ "-DCONFIG_VERSION='\"%s\"'" % version ] )
|
|
@ -1,157 +1,88 @@
|
|||
/**
|
||||
/*
|
||||
* Copyright 2013 (c) Anna Schumaker.
|
||||
*
|
||||
* The gst_init() function parses command line options passed to Ocarina
|
||||
* through argv. Use the command `gst-inspect-1.0 --help-gst` to find
|
||||
* what options are supported.
|
||||
*/
|
||||
#ifndef OCARINA_CORE_AUDIO_H
|
||||
#define OCARINA_CORE_AUDIO_H
|
||||
|
||||
extern "C" {
|
||||
#include <core/tags/track.h>
|
||||
}
|
||||
#include <string>
|
||||
#include <stdint.h>
|
||||
#include <gst/gst.h>
|
||||
|
||||
|
||||
/**
|
||||
* The audio driver class gives us an interface for using multiple
|
||||
* audio frameworks for audio playback.
|
||||
*/
|
||||
class AudioDriver {
|
||||
protected:
|
||||
void (*on_error) (); /**< Error handling callback function. */
|
||||
struct audio_callbacks {
|
||||
/* Called when a track is loaded. */
|
||||
void (*audio_cb_load)(struct track *);
|
||||
|
||||
public:
|
||||
AudioDriver(); /**< Default AudioDriver constructor. */
|
||||
virtual ~AudioDriver(); /**< AudioDriver destructor. */
|
||||
/* Called when playback state changes. */
|
||||
void (*audio_cb_state_change)(GstState);
|
||||
|
||||
|
||||
/**
|
||||
* Loads an audio file for playback.
|
||||
*
|
||||
* @param track The Track to load.
|
||||
*/
|
||||
virtual void load(struct track *track) = 0;
|
||||
|
||||
/**
|
||||
* Called to begin playback on the currently loaded track.
|
||||
*/
|
||||
virtual void play() = 0;
|
||||
|
||||
/**
|
||||
* Called to pause playback on the currently loaded track.
|
||||
*/
|
||||
virtual void pause() = 0;
|
||||
|
||||
/**
|
||||
* Called to check if the audio library is currently playing a track.
|
||||
*
|
||||
* @return True if audio library is playing, false otherwise.
|
||||
*/
|
||||
virtual bool is_playing() = 0;
|
||||
|
||||
|
||||
/**
|
||||
* Seek to a specific position in the current track.
|
||||
*
|
||||
* @param offset Position from the beginning of the track where we will seek to, in nanoseconds.
|
||||
*/
|
||||
virtual void seek_to(int64_t offset) = 0;
|
||||
|
||||
/**
|
||||
* Return the current position of the playback.
|
||||
*
|
||||
* @return The current playback position, in nanoseconds.
|
||||
*/
|
||||
virtual int64_t position() = 0;
|
||||
|
||||
/**
|
||||
* Return the duration of the currently loaded track.
|
||||
*
|
||||
* @return The duration of the current track, in nanoseconds.
|
||||
*/
|
||||
virtual int64_t duration() = 0;
|
||||
|
||||
/**
|
||||
* Called to handle reaching the end-of-stream.
|
||||
*/
|
||||
void eos();
|
||||
/* Called when the automatic pause state changes. */
|
||||
void (*audio_cb_config_pause)(int);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Namespace for higher-level audio controls.
|
||||
/* Called to initialize the audio manager. */
|
||||
void audio_init(int *, char ***, struct audio_callbacks *);
|
||||
|
||||
/* Called to deinitialize the audio manager. */
|
||||
void audio_deinit();
|
||||
|
||||
/* Called to force-save the current track. */
|
||||
void audio_save();
|
||||
|
||||
|
||||
/* Called to load either a track or file for playback. */
|
||||
bool audio_load(struct track *);
|
||||
bool audio_load_filepath(const gchar *);
|
||||
|
||||
/* Called to get the current track. */
|
||||
struct track *audio_cur_track();
|
||||
|
||||
/* Called to get the current playback state. */
|
||||
GstState audio_cur_state();
|
||||
|
||||
|
||||
/* Called to set the playback volume. */
|
||||
void audio_set_volume(unsigned int);
|
||||
|
||||
/* Called to get the playback volume. */
|
||||
unsigned int audio_get_volume();
|
||||
|
||||
|
||||
/* Called to begin playback. */
|
||||
bool audio_play();
|
||||
|
||||
/* Called to pause playback. */
|
||||
bool audio_pause();
|
||||
|
||||
/* Called to seek playback to a specific offset, in nanoseconds. */
|
||||
bool audio_seek(gint64);
|
||||
|
||||
/* Called to find the current playback position, in nanoseconds. */
|
||||
gint64 audio_position();
|
||||
|
||||
/* Called to find the duration of the current track. */
|
||||
gint64 audio_duration();
|
||||
|
||||
|
||||
/* Called to load the next track. */
|
||||
struct track *audio_next();
|
||||
|
||||
/* Called to load the previous track. */
|
||||
struct track *audio_prev();
|
||||
|
||||
/*
|
||||
* Called to configure automatic pausing.
|
||||
* Returns true if the value has been changed.
|
||||
*/
|
||||
namespace audio
|
||||
{
|
||||
|
||||
void init(); /**< Initializes the audio layer. */
|
||||
void play(); /**< Begin playback. */
|
||||
void pause(); /**< Pause playback. */
|
||||
|
||||
/**
|
||||
* Seek to a specific point in the track.
|
||||
*
|
||||
* @param pos Offset (in nanoseconds) from the beginning of the track.
|
||||
*/
|
||||
void seek_to(int64_t);
|
||||
|
||||
/**
|
||||
* Stop playback and seek to the beginning of the track.
|
||||
*/
|
||||
void stop();
|
||||
|
||||
/**
|
||||
* @return The current position of the audio playback (in nanoseconds).
|
||||
*/
|
||||
int64_t position();
|
||||
|
||||
/**
|
||||
* @return The duration of the currently loaded track (in nanoseconds).
|
||||
*/
|
||||
int64_t duration();
|
||||
|
||||
void next(); /**< Find and load the next track that should be played. */
|
||||
void prev(); /**< Call the deck :: previous() function and load the result. */
|
||||
|
||||
/**
|
||||
* Load a specific track for playback.
|
||||
*
|
||||
* @param track The track that should be loaded.
|
||||
*/
|
||||
void load_track(struct track *track);
|
||||
|
||||
/**
|
||||
* @return A pointer to the currently playing track object.
|
||||
*/
|
||||
struct track *current_track();
|
||||
|
||||
/**
|
||||
* Configure the automatic pausing feature.
|
||||
*
|
||||
* @param enabled Set to true to enable pausing, false to disable.
|
||||
* @param n Number of tracks to play before pausing.
|
||||
*/
|
||||
void pause_after(bool, unsigned int);
|
||||
|
||||
/**
|
||||
* Call to find the current automatic pausing state.
|
||||
*
|
||||
* @return True if automatic pausing is enabled, false otherwise.
|
||||
*/
|
||||
bool pause_enabled();
|
||||
|
||||
/**
|
||||
* Call to find the number of tracks remaining before pausing.
|
||||
*
|
||||
* @return The number of tracks before pausing.
|
||||
*/
|
||||
unsigned int pause_count();
|
||||
|
||||
/**
|
||||
* Called to access an audio driver.
|
||||
*
|
||||
* @return The current driver used by the application.
|
||||
*/
|
||||
AudioDriver *get_driver();
|
||||
};
|
||||
bool audio_pause_after(int);
|
||||
int audio_get_pause_count(void);
|
||||
|
||||
#ifdef CONFIG_TESTING
|
||||
void test_audio_eos();
|
||||
void test_audio_error(GError *, gchar *);
|
||||
GstElement *test_audio_pipeline();
|
||||
#endif /* CONFIG_TESTING */
|
||||
#endif /* OCARINA_CORE_AUDIO_H */
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
/*
|
||||
* Copyright 2014 (c) Anna Schumaker.
|
||||
*
|
||||
* The struct index_entry is used to associate a database key
|
||||
* with a set of integers, creating an inverted index.
|
||||
*/
|
||||
#ifndef OCARINA_CORE_CONTAINERS_INDEX_H
|
||||
#define OCARINA_CORE_CONTAINERS_INDEX_H
|
||||
|
||||
#include <core/containers/database.h>
|
||||
#include <core/containers/set.h>
|
||||
|
||||
|
||||
struct index_entry {
|
||||
gchar *ie_key;
|
||||
struct set ie_set;
|
||||
struct db_entry ie_dbe;
|
||||
};
|
||||
|
||||
#define INDEX_ENTRY(dbe) ((struct index_entry *)DBE_DATA(dbe))
|
||||
|
||||
|
||||
/* Initialize a database for use as an index. */
|
||||
void index_init(struct database *, const gchar *, bool);
|
||||
|
||||
/* Add a value to an index item with the specified key. */
|
||||
struct index_entry *index_insert(struct database *, const gchar *, unsigned int);
|
||||
|
||||
/* Remove a value from an index item with the specified key. */
|
||||
void index_remove(struct database *, const gchar *, unsigned int);
|
||||
|
||||
/* Called to check if the index has the specified (key, value) pair. */
|
||||
bool index_has(struct database *, const gchar *, unsigned int);
|
||||
|
||||
#ifdef CONFIG_TESTING
|
||||
const struct db_ops *test_index_ops();
|
||||
#endif /* CONFIG_TESTING */
|
||||
#endif /* OCARINA_CORE_CONTAINERS_INDEX_H */
|
|
@ -1,108 +0,0 @@
|
|||
/*
|
||||
* Copyright 2015 (c) Anna Schumaker.
|
||||
*/
|
||||
#ifndef OCARINA_CORE_CONTAINERS_QUEUE_H
|
||||
#define OCARINA_CORE_CONTAINERS_QUEUE_H
|
||||
|
||||
#include <glib.h>
|
||||
|
||||
struct _queue {
|
||||
GQueue _queue;
|
||||
};
|
||||
|
||||
struct _q_iter {
|
||||
guint it_pos;
|
||||
GList *it_iter;
|
||||
};
|
||||
|
||||
|
||||
/* Called to initialize a queue iterator. */
|
||||
static inline void _q_iter_init(const struct _queue *queue, struct _q_iter *it)
|
||||
{
|
||||
it->it_iter = g_list_first(queue->_queue.head);
|
||||
it->it_pos = 0;
|
||||
}
|
||||
|
||||
/* Called to advance a queue iterator by one step. */
|
||||
static inline void _q_iter_next(struct _q_iter *it)
|
||||
{
|
||||
it->it_iter = g_list_next(it->it_iter);
|
||||
it->it_pos++;
|
||||
}
|
||||
|
||||
/* Called to rewind a queue iterator by one step. */
|
||||
static inline void _q_iter_prev(struct _q_iter *it)
|
||||
{
|
||||
it->it_iter = g_list_previous(it->it_iter);
|
||||
it->it_pos--;
|
||||
}
|
||||
|
||||
/* Called to set a queue iterator to a specific position. */
|
||||
static inline void _q_iter_set(struct _queue *queue, struct _q_iter *it,
|
||||
unsigned int pos)
|
||||
{
|
||||
it->it_iter = g_queue_peek_nth_link(&queue->_queue, pos);
|
||||
it->it_pos = pos;
|
||||
}
|
||||
|
||||
/* Called to access the value of a queue iterator. */
|
||||
static inline gpointer _q_iter_val(struct _q_iter *it)
|
||||
{
|
||||
return (it->it_iter) ? it->it_iter->data : NULL;
|
||||
}
|
||||
|
||||
#define _q_for_each(queue, it) \
|
||||
for (_q_iter_init(queue, it); (it)->it_iter; _q_iter_next(it))
|
||||
|
||||
|
||||
#define _Q_INIT() \
|
||||
{ \
|
||||
._queue = G_QUEUE_INIT, \
|
||||
}
|
||||
|
||||
/* Called to initialize a queue. */
|
||||
static inline void _q_init(struct _queue *queue)
|
||||
{
|
||||
g_queue_init(&queue->_queue);
|
||||
}
|
||||
|
||||
/* Called to find the size of a queue. */
|
||||
static inline guint _q_size(struct _queue *queue)
|
||||
{
|
||||
return g_queue_get_length(&queue->_queue);
|
||||
}
|
||||
|
||||
/* Called to add an item to the head of a queue. */
|
||||
static inline guint _q_add_head(struct _queue *queue, gpointer data)
|
||||
{
|
||||
g_queue_push_head(&queue->_queue, data);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Called to add an item to the tail of a queue. */
|
||||
static inline guint _q_add_tail(struct _queue *queue, gpointer data)
|
||||
{
|
||||
g_queue_push_tail(&queue->_queue, data);
|
||||
return _q_size(queue) - 1;
|
||||
}
|
||||
|
||||
/* Called to add an item to a sorted queue. */
|
||||
guint _q_add_sorted(struct _queue *, gpointer, GCompareDataFunc, gpointer);
|
||||
|
||||
/* Called to remove all items from a queue */
|
||||
static inline void _q_clear(struct _queue *queue)
|
||||
{
|
||||
g_queue_clear(&queue->_queue);
|
||||
}
|
||||
|
||||
/* Called to remove an item by iterator. */
|
||||
gpointer _q_remove_it(struct _queue *, struct _q_iter *);
|
||||
|
||||
/* Called to sort the queue. */
|
||||
static inline void _q_sort(struct _queue *queue, GCompareDataFunc func,
|
||||
gpointer user_data)
|
||||
{
|
||||
g_queue_sort(&queue->_queue, func, user_data);
|
||||
}
|
||||
|
||||
#endif /* OCARINA_CORE_CONTAINERS_QUEUE_H */
|
|
@ -1,91 +0,0 @@
|
|||
/*
|
||||
* Copyright 2015 (c) Anna Schumaker.
|
||||
*/
|
||||
#ifndef OCARINA_CORE_CONTAINERS_SET_H
|
||||
#define OCARINA_CORE_CONTAINERS_SET_H
|
||||
|
||||
#include <core/file.h>
|
||||
#include <glib.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
|
||||
struct set {
|
||||
GHashTable *s_set;
|
||||
};
|
||||
|
||||
struct set_iter {
|
||||
unsigned int it_val;
|
||||
GHashTableIter it_iter;
|
||||
};
|
||||
|
||||
|
||||
#define SET_INIT() \
|
||||
{ \
|
||||
.s_set = g_hash_table_new(g_direct_hash, g_direct_equal), \
|
||||
}
|
||||
|
||||
static inline void set_init(struct set *set)
|
||||
{
|
||||
set->s_set = g_hash_table_new(g_direct_hash, g_direct_equal);
|
||||
}
|
||||
|
||||
static inline void set_deinit(struct set *set)
|
||||
{
|
||||
g_hash_table_destroy(set->s_set);
|
||||
}
|
||||
|
||||
static inline void set_insert(struct set *set, unsigned int value)
|
||||
{
|
||||
g_hash_table_add(set->s_set, GUINT_TO_POINTER(value));
|
||||
}
|
||||
|
||||
static inline void set_remove(struct set *set, unsigned int value)
|
||||
{
|
||||
g_hash_table_remove(set->s_set, GUINT_TO_POINTER(value));
|
||||
}
|
||||
|
||||
static inline void set_clear(struct set *set)
|
||||
{
|
||||
g_hash_table_remove_all(set->s_set);
|
||||
}
|
||||
|
||||
static inline bool set_has(const struct set *set, unsigned int value)
|
||||
{
|
||||
return g_hash_table_contains(set->s_set, GUINT_TO_POINTER(value));
|
||||
}
|
||||
|
||||
static inline unsigned int set_size(struct set *set)
|
||||
{
|
||||
return g_hash_table_size(set->s_set);
|
||||
}
|
||||
|
||||
/* Copy values from set1 into set2. */
|
||||
void set_copy(const struct set *, struct set *);
|
||||
|
||||
/* Remove values from set2 that are not also in set1. */
|
||||
void set_inline_intersect(const struct set *, struct set *);
|
||||
|
||||
/* Read values from file. */
|
||||
void set_read(struct file *, struct set *);
|
||||
|
||||
/* Write values to file. */
|
||||
void set_write(struct file *, struct set *);
|
||||
|
||||
|
||||
static inline bool set_iter_next(struct set_iter *it)
|
||||
{
|
||||
gpointer key;
|
||||
bool ret = g_hash_table_iter_next(&it->it_iter, &key, NULL);
|
||||
it->it_val = GPOINTER_TO_INT(key);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static inline void set_iter_init(const struct set *set, struct set_iter *it)
|
||||
{
|
||||
g_hash_table_iter_init(&it->it_iter, set->s_set);
|
||||
}
|
||||
|
||||
#define set_for_each(set, it) \
|
||||
for (set_iter_init(set, it); set_iter_next(it); )
|
||||
|
||||
#endif /* OCARINA_CORE_CONTAINERS_SET_H */
|
|
@ -1,32 +1,20 @@
|
|||
/**
|
||||
/*
|
||||
* Copyright 2014 (c) Anna Schumaker.
|
||||
*/
|
||||
#ifndef OCARINA_CORE_CORE_H
|
||||
#define OCARINA_CORE_CORE_H
|
||||
#include <core/audio.h>
|
||||
#include <core/idle.h>
|
||||
#include <core/playlist.h>
|
||||
#include <core/settings.h>
|
||||
#include <core/tags/tags.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#include <core/queue.h>
|
||||
/* Called to initialize all core Ocarina components. */
|
||||
void core_init(int *, char ***, struct playlist_callbacks *,
|
||||
struct audio_callbacks *, enum idle_sync_t);
|
||||
|
||||
|
||||
struct core_init_data {
|
||||
struct queue_ops *collection_ops;
|
||||
struct queue_ops *history_ops;
|
||||
struct queue_ops *playlist_ops;
|
||||
struct queue_ops *tempq_ops;
|
||||
};
|
||||
|
||||
/**
|
||||
* Namespace for basic core library functions.
|
||||
*/
|
||||
namespace core
|
||||
{
|
||||
|
||||
/**
|
||||
* Initializes all components of the core library, including reading
|
||||
* databases from disk and setting up gstreamer.
|
||||
*/
|
||||
void init(struct core_init_data *);
|
||||
void deinit();
|
||||
|
||||
}
|
||||
/* Called to deinitialize all core Ocarina componentns. */
|
||||
void core_deinit();
|
||||
|
||||
#endif /* OCARINA_CORE_CORE_H */
|
||||
|
|
|
@ -48,7 +48,7 @@ static inline void *DBE_DATA(struct db_entry *dbe)
|
|||
|
||||
struct db_ops {
|
||||
/* Allocate a new struct db_entry from a given key. */
|
||||
struct db_entry *(*dbe_alloc)(const gchar *);
|
||||
struct db_entry *(*dbe_alloc)(const gchar *, unsigned int);
|
||||
|
||||
/* Free a struct db_entry. */
|
||||
void (*dbe_free)(struct db_entry *);
|
||||
|
@ -57,10 +57,7 @@ struct db_ops {
|
|||
gchar *(*dbe_key)(struct db_entry *);
|
||||
|
||||
/* Read a single struct db_entry from disk. */
|
||||
struct db_entry *(*dbe_read)(struct file *);
|
||||
|
||||
/* Set up a struct db_entry after adding to the database. */
|
||||
void (*dbe_setup)(struct db_entry *);
|
||||
struct db_entry *(*dbe_read)(struct file *, unsigned int);
|
||||
|
||||
/* Write a single struct db_entry to disk. */
|
||||
void (*dbe_write)(struct file *, struct db_entry *);
|
||||
|
@ -77,11 +74,11 @@ struct database {
|
|||
const struct db_ops *db_ops; /* The database's operations vector. */
|
||||
};
|
||||
|
||||
#define DB_INIT(fname, autosave, ops) \
|
||||
#define DB_INIT(fname, autosave, ops, fmin) \
|
||||
{ \
|
||||
.db_size = 0, \
|
||||
.db_autosave = autosave, \
|
||||
.db_file = FILE_INIT(fname, 0), \
|
||||
.db_file = FILE_INIT_DATA("", fname, fmin), \
|
||||
.db_entries = g_ptr_array_new(), \
|
||||
.db_keys = g_hash_table_new(g_str_hash, g_str_equal), \
|
||||
.db_ops = ops, \
|
||||
|
@ -92,7 +89,8 @@ struct database {
|
|||
* Initialize a database using filepath as a location on disk to store data
|
||||
* and autosave as a hint for if this database should be automatically saved.
|
||||
*/
|
||||
void db_init(struct database *, const char *, bool, const struct db_ops *);
|
||||
void db_init(struct database *, const char *, bool, const struct db_ops *,
|
||||
unsigned int);
|
||||
|
||||
/* Called to prevent memory leaks by freeing all remaining database entries. */
|
||||
void db_deinit(struct database *);
|
||||
|
@ -119,9 +117,18 @@ struct db_entry *db_insert(struct database *, const gchar *);
|
|||
/* Called to remove an item from the database. */
|
||||
void db_remove(struct database *, struct db_entry *);
|
||||
|
||||
/*
|
||||
* Called to shrink the database by removing any NULL pointers without
|
||||
* changing the order of items in the database.
|
||||
* Returns true if the database has been modified.
|
||||
*/
|
||||
bool db_defrag(struct database *);
|
||||
|
||||
/* Called to change the key of a database entry. */
|
||||
void db_rekey(struct database *, struct db_entry *);
|
||||
|
||||
/* Returns the database item at the requested index. */
|
||||
struct db_entry *db_at(struct database *, unsigned int);
|
||||
struct db_entry *db_at(const struct database *, unsigned int);
|
||||
|
||||
/* Returns the database item with the specified key. */
|
||||
struct db_entry *db_get(struct database *, const gchar *);
|
|
@ -5,12 +5,18 @@
|
|||
#define OCARINA_CORE_DATE_H
|
||||
|
||||
#include <core/file.h>
|
||||
#include <stdint.h>
|
||||
|
||||
|
||||
struct date {
|
||||
unsigned int d_year;
|
||||
unsigned int d_month;
|
||||
unsigned int d_day;
|
||||
union {
|
||||
struct {
|
||||
uint16_t d_year;
|
||||
uint8_t d_month;
|
||||
uint8_t d_day;
|
||||
};
|
||||
uint32_t d_stamp;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
|
@ -22,9 +28,11 @@ void date_today(struct date *);
|
|||
|
||||
/* Read the date from file. */
|
||||
void date_read(struct file *, struct date *);
|
||||
void date_read_stamp(struct file *, struct date *);
|
||||
|
||||
/* Write the date to file. */
|
||||
void date_write(struct file *, struct date *);
|
||||
void date_write_stamp(struct file *, struct date *);
|
||||
|
||||
/*
|
||||
* Convert the date into a string.
|
||||
|
|
|
@ -1,120 +0,0 @@
|
|||
/**
|
||||
* Copyright 2013 (c) Anna Schumaker.
|
||||
*/
|
||||
#ifndef OCARINA_CORE_DECK_H
|
||||
#define OCARINA_CORE_DECK_H
|
||||
|
||||
extern "C" {
|
||||
#include <core/queue.h>
|
||||
}
|
||||
#include <list>
|
||||
|
||||
|
||||
/**
|
||||
* A TempQueue is a wrapper around the Queue class that
|
||||
* allows us to save the list of queues when necessary.
|
||||
*/
|
||||
class TempQueue : public queue
|
||||
{
|
||||
public:
|
||||
TempQueue();
|
||||
TempQueue(bool, struct queue_ops *);
|
||||
|
||||
void read(file &);
|
||||
void write(file &);
|
||||
|
||||
unsigned int add(struct track *);
|
||||
void del(struct track *);
|
||||
void del(unsigned int);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* The deck is used to hold temporary queues created by the user. This
|
||||
* code is also in charge of maintaining a "recently played" queue of
|
||||
* songs that have just played.
|
||||
*
|
||||
* When saving to disk:
|
||||
* ... << deck.size() << endl;
|
||||
* ... << deck[0] << endl;
|
||||
* ...
|
||||
* ... << deck[N] <<< endl;
|
||||
*/
|
||||
namespace deck
|
||||
{
|
||||
|
||||
/**
|
||||
* Read the deck file from disk and restore the queues.
|
||||
*/
|
||||
void init(struct queue_ops *, struct queue_ops *);
|
||||
|
||||
/**
|
||||
* Save the current queues to a file on disk.
|
||||
*/
|
||||
void write();
|
||||
void save(struct queue *, enum queue_flags);
|
||||
|
||||
/**
|
||||
* Create a new queue at the end of the deck.
|
||||
*
|
||||
* @param random Set to true if the new queue should return a random
|
||||
* track when queue->next() is called.
|
||||
* @return The newly created queue.
|
||||
*/
|
||||
queue *create(bool, struct queue_ops *);
|
||||
|
||||
/**
|
||||
* Removes the queue from the deck.
|
||||
*
|
||||
* @param queue The queue to be removed.
|
||||
*/
|
||||
void destroy(queue *);
|
||||
|
||||
/**
|
||||
* Move the queue to a new location in the deck.
|
||||
*
|
||||
* @param queue The queue to be moved.
|
||||
* @param index The new index of the queue.
|
||||
*/
|
||||
void move(queue *, unsigned int);
|
||||
|
||||
/**
|
||||
* Find the index of the requested queue.
|
||||
*
|
||||
* @param queue The queue in question.
|
||||
* @return The index of the requested queue.
|
||||
*/
|
||||
unsigned int index(queue *);
|
||||
|
||||
/**
|
||||
* Access the queue at the specified index.
|
||||
*
|
||||
* @param index The index of the queue that should be accessed.
|
||||
* @return The queue at the requested index.
|
||||
*/
|
||||
queue *get(unsigned int);
|
||||
|
||||
/**
|
||||
* @return A track from the first enabled queue. If no queues are
|
||||
* enabled, return a track from the Library.
|
||||
*/
|
||||
struct track *next();
|
||||
|
||||
/**
|
||||
* @return A track from the recent tracks queue.
|
||||
*/
|
||||
struct track *prev();
|
||||
|
||||
/**
|
||||
* @return The deck of queues.
|
||||
*/
|
||||
std::list<TempQueue> &get_queues();
|
||||
|
||||
/**
|
||||
* @return The queue of recent tracks.
|
||||
*/
|
||||
queue *get_queue();
|
||||
|
||||
};
|
||||
|
||||
#endif /* OCARINA_CORE_DECK_H */
|
|
@ -23,44 +23,61 @@
|
|||
#ifndef OCARINA_CORE_FILE_H
|
||||
#define OCARINA_CORE_FILE_H
|
||||
|
||||
#include <core/version.h>
|
||||
#include <glib.h>
|
||||
#include <glib/gstdio.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
|
||||
#define FILE_MAX_LEN 16
|
||||
|
||||
enum open_mode {
|
||||
OPEN_READ,
|
||||
OPEN_WRITE,
|
||||
CLOSED, /* File is not open. */
|
||||
OPEN_READ, /* File is open for reading text. */
|
||||
OPEN_READ_BINARY, /* File is open for reading binary data. */
|
||||
OPEN_WRITE, /* File is open for writing text. */
|
||||
OPEN_WRITE_BINARY, /* File is open for writing binary data. */
|
||||
};
|
||||
|
||||
struct file {
|
||||
enum open_mode f_mode; /* The file's current open mode. */
|
||||
unsigned int f_version; /* The file's current data version. */
|
||||
unsigned int f_prev; /* The file's on-disk data version. */
|
||||
FILE *f_file; /* The file's IO stream. */
|
||||
gchar f_name[FILE_MAX_LEN]; /* The file's basename. */
|
||||
FILE *f_file; /* The file's IO stream. */
|
||||
const gchar *f_name; /* The file's basename. */
|
||||
const gchar *f_subdir; /* The file's subdirectory. */
|
||||
|
||||
enum open_mode f_mode; /* The file's current open mode. */
|
||||
unsigned int f_version; /* The file's current data version. */
|
||||
unsigned int f_prev; /* The file's on-disk data version. */
|
||||
unsigned int f_min; /* The file's minimum data version. */
|
||||
|
||||
const gchar *(*f_user_dir)(void); /* The file's user directory. */
|
||||
};
|
||||
|
||||
#define FILE_INIT(fname, version) \
|
||||
{ \
|
||||
.f_mode = OPEN_READ, \
|
||||
.f_version = version, \
|
||||
.f_prev = 0, .f_file = NULL, \
|
||||
.f_name = fname, \
|
||||
#define FILE_INIT_DATA(fdir, fname, min) \
|
||||
{ \
|
||||
.f_file = NULL, \
|
||||
.f_name = fname, \
|
||||
.f_subdir = fdir, \
|
||||
.f_mode = CLOSED, \
|
||||
.f_version = OCARINA_MINOR_VERSION, \
|
||||
.f_prev = 0, \
|
||||
.f_min = min, \
|
||||
.f_user_dir = g_get_user_data_dir, \
|
||||
}
|
||||
|
||||
|
||||
/* Initialize a new file object. */
|
||||
void file_init(struct file *, const gchar *, unsigned int);
|
||||
/* Initialize a file object. */
|
||||
void file_init_data(struct file *, const gchar *, const gchar *, unsigned int);
|
||||
void file_init_cache(struct file *, const gchar *, const gchar *);
|
||||
|
||||
/*
|
||||
* Returns the full path of the file or an empty string if filename is not set.
|
||||
* This function allocates a new string that MUST be freed with g_free().
|
||||
* NOTE: This function allocates a new string that MUST be freed with g_free().
|
||||
*/
|
||||
gchar *file_path(struct file *);
|
||||
|
||||
/*
|
||||
* Returns the path to the temporary file used for writes.
|
||||
* NOTE: This function allocates a new string that MUST be freed with g_free().
|
||||
*/
|
||||
gchar *file_write_path(struct file *);
|
||||
|
||||
/* Returns the version number of the file. */
|
||||
const unsigned int file_version(struct file *);
|
||||
|
||||
|
@ -71,32 +88,42 @@ bool file_exists(struct file *);
|
|||
* Call to open a file for either reading or writing. Callers
|
||||
* are expected to call file_close() when IO is completed.
|
||||
*
|
||||
* When opening a file for reading (OPEN_READ):
|
||||
* - Check if the file exists.
|
||||
* - Read in file->_prev_version from the start of the file.
|
||||
* When opening a file for reading (OPEN_READ / OPEN_READ_BINARY):
|
||||
* - Check if the file exists
|
||||
* - If open for reading text (OPEN_READ):
|
||||
* - Read in file->_prev_version from the start of the file.
|
||||
*
|
||||
* When opening a file for writing (OPEN_WRITE):
|
||||
* When opening a file for writing (OPEN_WRITE / OPEN_WRITE_BINARY):
|
||||
* - Create missing directories as needed.
|
||||
* - Write file->_version to the start of the file.
|
||||
* - Open a temporary file to protect data if Ocarina crashes.
|
||||
* - If open for writing text (OPEN_WRITE):
|
||||
* - Write file->_version to the start of the file (data files only).
|
||||
*
|
||||
* Returns true if the open was successful and false otherwise.
|
||||
*/
|
||||
bool file_open(struct file *, enum open_mode);
|
||||
|
||||
/* Close an open file, setting file->f_file to NULL. */
|
||||
/*
|
||||
* Closes an open file, setting file->f_file to NULL and file->f_mode
|
||||
* to CLOSED. If the file was opened for writing, then rename the
|
||||
* temporary file to file_path().
|
||||
*/
|
||||
void file_close(struct file *);
|
||||
|
||||
/*
|
||||
* Read an entire line from the file and return it to the caller.
|
||||
* This function allocates a new string that MUST be freed with g_free().
|
||||
* Called to read an unsigned int, signed int, single word, or entire
|
||||
* line from the file.
|
||||
* NOTE: file_readw() and file_readl() both return a new string that
|
||||
* MUST be freed with g_free()
|
||||
*/
|
||||
gchar *file_readw(struct file *);
|
||||
gchar *file_readl(struct file *);
|
||||
|
||||
/*
|
||||
* Read from a file with an fscanf(3) style format string.
|
||||
* Returns the number of items matched.
|
||||
*/
|
||||
int file_readf(struct file *, const char *, ...);
|
||||
unsigned int file_readu(struct file *);
|
||||
static inline int file_readd(struct file *file)
|
||||
{ return (int)file_readu(file); }
|
||||
static inline unsigned short int file_readhu(struct file *file)
|
||||
{ return (unsigned short int)file_readu(file); }
|
||||
|
||||
/*
|
||||
* Write to a file with an fprintf(3) style format string.
|
||||
|
@ -104,4 +131,22 @@ int file_readf(struct file *, const char *, ...);
|
|||
*/
|
||||
int file_writef(struct file *, const char *, ...);
|
||||
|
||||
/*
|
||||
* Reads the contents of a file as binary data.
|
||||
* NOTE: This function returns a new string which MUST be freed with g_free().
|
||||
*/
|
||||
gchar *file_read(struct file *);
|
||||
|
||||
/*
|
||||
* Write binary data a cache file, similar to fwrite(3).
|
||||
* Returns the number of bytes successfully written.
|
||||
*/
|
||||
int file_write(struct file *, const void *, size_t);
|
||||
|
||||
/* Import a file into the cache. */
|
||||
bool file_import(struct file *, const gchar *);
|
||||
|
||||
/* Removes a closed file from disk. */
|
||||
bool file_remove(struct file *);
|
||||
|
||||
#endif /* OCARINA_CORE_FILE_H */
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
/*
|
||||
* Copyright 2013 (c) Anna Schumaker.
|
||||
*
|
||||
* The filter layer is used to search for a subset of
|
||||
* songs based on an input string.
|
||||
*/
|
||||
#ifndef OCARINA_CORE_FILTER_H
|
||||
#define OCARINA_CORE_FILTER_H
|
||||
|
||||
#include <core/containers/set.h>
|
||||
|
||||
/* Called to initialize the filter index. */
|
||||
void filter_init();
|
||||
|
||||
/* Called to clean up the filter index. */
|
||||
void filter_deinit();
|
||||
|
||||
/* Add the input string to the index. */
|
||||
void filter_add(const gchar *, unsigned int);
|
||||
|
||||
/* Search for the input string in the index. */
|
||||
void filter_search(const gchar *, struct set *);
|
||||
|
||||
#endif /* OCARINA_CORE_FILTER_H */
|
|
@ -15,8 +15,22 @@
|
|||
#define OCARINA_CORE_IDLE_H
|
||||
#include <stdbool.h>
|
||||
|
||||
enum idle_sync_t {
|
||||
IDLE_SYNC, /* Run task in the main thread. */
|
||||
IDLE_ASYNC, /* Run task in a separate thread. */
|
||||
};
|
||||
|
||||
#define IDLE_FUNC(x) ((bool (*)(void *))x)
|
||||
|
||||
|
||||
/* Called to initialize the idle queue. */
|
||||
void idle_init(enum idle_sync_t);
|
||||
|
||||
/* Called to deinitialize the idle queue. */
|
||||
void idle_deinit();
|
||||
|
||||
/* Called to schedule a function to run later. */
|
||||
void idle_schedule(void (*)(void *), void *);
|
||||
void idle_schedule(enum idle_sync_t, bool (*)(void *), void *);
|
||||
|
||||
/*
|
||||
* Called to run the next task on the idle queue.
|
||||
|
|
|
@ -1,87 +0,0 @@
|
|||
/**
|
||||
* Copyright 2013 (c) Anna Schumaker.
|
||||
*/
|
||||
#ifndef OCARINA_CORE_LIBRARY_H
|
||||
#define OCARINA_CORE_LIBRARY_H
|
||||
|
||||
extern "C" {
|
||||
#include <core/queue.h>
|
||||
}
|
||||
#include <string>
|
||||
|
||||
|
||||
/**
|
||||
* The Library is in charge of scanning and updating Library tags along
|
||||
* with every other tag in the tag database. This code will also manage
|
||||
* a special Queue used by the UI to display all enabled Tracks.
|
||||
*
|
||||
* The library queue is dynamic, so saving involves only storing the current
|
||||
* flags and sort order.
|
||||
*
|
||||
* ... << flags << _sort_order.size()
|
||||
* ... << _sort_order[N].field << _sort_order[N].ascending << ...
|
||||
*/
|
||||
namespace collection
|
||||
{
|
||||
|
||||
/**
|
||||
* Scan over every Track tag and add each enabled Track to the
|
||||
* library queue.
|
||||
*/
|
||||
void init(struct queue_ops *);
|
||||
|
||||
void save(struct queue *, enum queue_flags);
|
||||
|
||||
/**
|
||||
* Add a new directory to the library.
|
||||
*
|
||||
* @param dir The directory that should be scanned.
|
||||
* @return The newly created Library tag or NULL if a
|
||||
* tag could not be created.
|
||||
*/
|
||||
struct library *add(const std::string &);
|
||||
|
||||
/**
|
||||
* Remove a Library tag from the database along with every
|
||||
* Track associated with this library.
|
||||
*
|
||||
* @param library The library path that should be removed.
|
||||
*/
|
||||
void remove(struct library *);
|
||||
|
||||
/**
|
||||
* First, scan over every Track in the database and remove
|
||||
* tracks that no longer exist in the filesystem.
|
||||
*
|
||||
* Next, scan over every file in the Library's root directory
|
||||
* and create new Track tags for every file found.
|
||||
*
|
||||
* @param library The library path that should be updated.
|
||||
*/
|
||||
void update(struct library *);
|
||||
|
||||
/**
|
||||
* Call library :: update() on all Library tags.
|
||||
*/
|
||||
void update_all();
|
||||
|
||||
/**
|
||||
* Use to enable or disable a library path. When a Library path
|
||||
* is disabled, its tracks will be removed from the Library queue.
|
||||
*
|
||||
* @param library The library to be enabled or disabled.
|
||||
* @param enabled Set to true if the library should be enabled,
|
||||
* and false to disable it.
|
||||
*/
|
||||
void set_enabled(struct library *, bool);
|
||||
|
||||
/**
|
||||
* Use to access the library queue.
|
||||
*
|
||||
* @return The queue of tracks currently in the library.
|
||||
*/
|
||||
queue *get_queue();
|
||||
|
||||
};
|
||||
|
||||
#endif /* OCARINA_CORE_LIBRARY_H */
|
|
@ -1,88 +1,81 @@
|
|||
/**
|
||||
/*
|
||||
* Copyright 2013 (c) Anna Schumaker.
|
||||
*
|
||||
* The playlist manager is in charge of the various playlists Ocarina
|
||||
* knows about. This code also manages a special queue used by the GUI
|
||||
* to display the tracks in each playlist.
|
||||
*/
|
||||
#ifndef OCARINA_CORE_PLAYLIST_H
|
||||
#define OCARINA_CORE_PLAYLIST_H
|
||||
#include <core/playlists/artist.h>
|
||||
#include <core/playlists/generic.h>
|
||||
#include <core/playlists/library.h>
|
||||
#include <core/playlists/system.h>
|
||||
#include <core/playlists/user.h>
|
||||
|
||||
extern "C" {
|
||||
#include <core/containers/index.h>
|
||||
#include <core/queue.h>
|
||||
}
|
||||
|
||||
#include <string>
|
||||
/* Called to initialize the playlist manager. */
|
||||
void playlist_init(struct playlist_callbacks *);
|
||||
|
||||
/**
|
||||
* The playlist namespace is in charge of managing the various playlists
|
||||
* Ocarina knows aboit. It is also in charge of a special queue that the
|
||||
* UI uses to display Tracks in each playlist.
|
||||
*
|
||||
* Currently supported playlists are:
|
||||
*
|
||||
* Name | Description
|
||||
* ----------|------------
|
||||
* Banned | Songs that the user doesn't like.
|
||||
* Favorites | Songs that the user likes.
|
||||
*/
|
||||
namespace playlist
|
||||
{
|
||||
/* Called to deinitialize the playlist manager. */
|
||||
void playlist_deinit();
|
||||
|
||||
/**
|
||||
* Read playlist information from disk and removed banned tracks
|
||||
* from the Library queue.
|
||||
*/
|
||||
void init(struct queue_ops *);
|
||||
/* Called to force-save all playlists. */
|
||||
void playlist_save();
|
||||
|
||||
/**
|
||||
* Check if a specific track is in a playlist.
|
||||
*
|
||||
* @param track The track in question.
|
||||
* @param name The name of the playlist to check.
|
||||
* @return True if the track is in the playlist, false otherwise.
|
||||
*/
|
||||
bool has(struct track *, const std::string &);
|
||||
/* Called to notify all playlists that a track has been played. */
|
||||
void playlist_played(struct track *);
|
||||
|
||||
/**
|
||||
* Add a track to a playlist.
|
||||
*
|
||||
* Tracks added to the Banned playlist will be removed from
|
||||
* the library queue.
|
||||
*
|
||||
* @param track The track to add.
|
||||
* @param name The name of the playlist to add to.
|
||||
*/
|
||||
void add(struct track *, const std::string &);
|
||||
/* Called to notify all playlists that a track has been selected. */
|
||||
void playlist_selected(struct track *);
|
||||
|
||||
/**
|
||||
* Remove a track from a playlist.
|
||||
*
|
||||
* Tracks removed from the Banned playlist will be added back
|
||||
* to the library queue.
|
||||
*
|
||||
* @param track The track to remove.
|
||||
* @param name The name of the playlist to remove from.
|
||||
*/
|
||||
void del(struct track *, const std::string &);
|
||||
|
||||
/**
|
||||
* Use to change the currently displayed playlist.
|
||||
*
|
||||
* @param name The name of the queue to queue up.
|
||||
*/
|
||||
void select(const std::string &);
|
||||
/* Called to create a new playlist. */
|
||||
struct playlist *playlist_new(enum playlist_type_t, const gchar *);
|
||||
|
||||
/**
|
||||
* Use to access specific tracks in a playlist.
|
||||
*
|
||||
* @param name The playlist to access.
|
||||
* @return The index_entry containing the tracks.
|
||||
*/
|
||||
index_entry *get_tracks(const std::string &);
|
||||
/* Called to delete a playlist. */
|
||||
bool playlist_delete(struct playlist *);
|
||||
|
||||
/**
|
||||
* @return The playlist queue.
|
||||
*/
|
||||
queue *get_queue();
|
||||
|
||||
};
|
||||
/* Called to look up playlists either by name or id. */
|
||||
struct playlist *playlist_lookup(enum playlist_type_t, const gchar *);
|
||||
struct playlist *playlist_get(enum playlist_type_t, unsigned int);
|
||||
|
||||
/* Called to access the current playlist. */
|
||||
struct playlist *playlist_current(void);
|
||||
|
||||
|
||||
/* Called to select the current playlist. */
|
||||
bool playlist_select(struct playlist *);
|
||||
|
||||
/* Called to get the next track from the default playlist. */
|
||||
struct track *playlist_next(void);
|
||||
|
||||
/* Called to get a previously played track. */
|
||||
struct track *playlist_prev(void);
|
||||
|
||||
|
||||
/* Called to add a track to a playlist. */
|
||||
bool playlist_add(struct playlist *, struct track *);
|
||||
|
||||
/* Called to remove a track from a playlist. */
|
||||
bool playlist_remove(struct playlist *, struct track *);
|
||||
|
||||
/* Called to check if a specific track is in the playlist. */
|
||||
bool playlist_has(struct playlist *, struct track *);
|
||||
|
||||
|
||||
/* Called to set the playlist's random flag. */
|
||||
void playlist_set_random(struct playlist *, bool);
|
||||
|
||||
/* Called to change the sort order of the playlist. */
|
||||
bool playlist_sort(struct playlist *, enum compare_t);
|
||||
|
||||
/* Called to manually rearrange the order of the playlist. */
|
||||
bool playlist_rearrange(struct playlist *, unsigned int, unsigned int);
|
||||
|
||||
|
||||
/* Called to set the playlist's search text */
|
||||
void playlist_set_search(struct playlist *, const gchar *);
|
||||
|
||||
#endif /* OCARINA_CORE_PLAYLIST_H */
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright 2016 (c) Anna Schumaker.
|
||||
*/
|
||||
#ifndef OCARINA_CORE_PLAYLISTS_ARTIST_H
|
||||
#define OCARINA_CORE_PLAYLISTS_ARTIST_H
|
||||
#include <core/playlists/generic.h>
|
||||
|
||||
/* Artist playlist type. */
|
||||
extern struct playlist_type pl_artist;
|
||||
|
||||
|
||||
/* Called to initialize artist playlists. */
|
||||
void pl_artist_init(void);
|
||||
|
||||
/* Called to deinitialize library playlists. */
|
||||
void pl_artist_deinit();
|
||||
|
||||
/* Called to tell system playlists about a new track. */
|
||||
void pl_artist_new_track(struct track *);
|
||||
|
||||
/* Called to tell artist playlists that a track is getting deleted. */
|
||||
void pl_artist_delete_track(struct track *);
|
||||
|
||||
#endif /* OCARINA_CORE_PLAYLISTS_ARTIST_H */
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* Copyright 2016 (c) Anna Schumaker.
|
||||
*/
|
||||
#ifndef OCARINA_CORE_PLAYLISTS_GENERIC_H
|
||||
#define OCARINA_CORE_PLAYLISTS_GENERIC_H
|
||||
#include <core/playlists/iterator.h>
|
||||
#include <core/playlists/playlist.h>
|
||||
|
||||
enum playlist_save_flags {
|
||||
PL_SAVE_FLAGS = (1 << 0), /* Save playlist random and sort data. */
|
||||
PL_SAVE_ITER = (1 << 1), /* Save playlist iterator position. */
|
||||
PL_SAVE_TRACKS = (1 << 2), /* Save playlist tracks. */
|
||||
};
|
||||
#define PL_SAVE_METADATA (PL_SAVE_FLAGS | PL_SAVE_ITER)
|
||||
#define PL_SAVE_ALL (PL_SAVE_TRACKS | PL_SAVE_METADATA)
|
||||
|
||||
struct playlist_callbacks {
|
||||
/* Called to notify that a new playlist has been allocated. */
|
||||
void (*pl_cb_alloc)(struct playlist *);
|
||||
|
||||
/* Called to notify that a track has been added. */
|
||||
void (*pl_cb_added)(struct playlist *, struct track *);
|
||||
|
||||
/*
|
||||
* Called to notify that N instances of a track have been removed.
|
||||
* Track may be NULL to indicate that several different tracks were
|
||||
* removed at once.
|
||||
*/
|
||||
void (*pl_cb_removed)(struct playlist *, struct track *, unsigned int n);
|
||||
|
||||
/*
|
||||
* Called to notify that a track has been updated.
|
||||
* If the track is NULL, then the entire playlist should be updated.
|
||||
*/
|
||||
void (*pl_cb_updated)(struct playlist *, struct track *);
|
||||
};
|
||||
|
||||
|
||||
/* Called to set playlist callbacks. */
|
||||
void playlist_generic_set_callbacks(struct playlist_callbacks *);
|
||||
|
||||
/* Generic playlist init functions. */
|
||||
void playlist_generic_init(struct playlist *, unsigned int, ...);
|
||||
|
||||
/* Generic playlist deinit function. */
|
||||
void playlist_generic_deinit(struct playlist *);
|
||||
|
||||
/* Generic playlist alloc function. */
|
||||
struct playlist *playlist_generic_alloc(gchar *, enum playlist_type_t,
|
||||
unsigned int, struct playlist_ops *,
|
||||
unsigned int, ...);
|
||||
|
||||
/* Generic playlist free function. */
|
||||
void playlist_generic_free(struct playlist *);
|
||||
|
||||
/* Generic playlist save function. */
|
||||
void playlist_generic_save(struct playlist *, struct file *, unsigned int);
|
||||
|
||||
/* Generic playlist load function. */
|
||||
void playlist_generic_load(struct playlist *, struct file *, unsigned int);
|
||||
|
||||
/* Generic playlist can-select function. */
|
||||
bool playlist_generic_can_select(struct playlist *);
|
||||
|
||||
/* Generic playlist clear operation. */
|
||||
void playlist_generic_clear(struct playlist *);
|
||||
|
||||
/* Generic playlist add track operations. */
|
||||
bool playlist_generic_add(struct playlist *, struct track *);
|
||||
bool playlist_generic_add_front(struct playlist *, struct track *);
|
||||
|
||||
/* Generic playlist remove track operation. */
|
||||
bool playlist_generic_remove(struct playlist *, struct track *);
|
||||
|
||||
/* Generic playlist update track operation. */
|
||||
void playlist_generic_update(struct playlist *, struct track *);
|
||||
|
||||
/* Generic playlist set_random operation. */
|
||||
void playlist_generic_set_random(struct playlist *, bool);
|
||||
|
||||
/* Generic playlist sorting operations. */
|
||||
void playlist_generic_sort(struct playlist *, enum compare_t);
|
||||
void playlist_generic_resort(struct playlist *);
|
||||
|
||||
/* Generic playlist rearranging operation. */
|
||||
bool playlist_generic_rearrange(struct playlist *, unsigned int, unsigned int);
|
||||
|
||||
/* Generic playlist next track operation. */
|
||||
struct track *playlist_generic_next(struct playlist *);
|
||||
|
||||
#endif /* OCARINA_CORE_PLAYLISTS_GENERIC_H */
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* Copyright 2016 (c) Anna Schumaker.
|
||||
*
|
||||
* NOTE: The playlist_iter type is defined in include/core/playlists/playlist.h
|
||||
*/
|
||||
#ifndef OCARINA_CORE_PLAYLISTS_ITERATOR_H
|
||||
#define OCARINA_CORE_PLAYLISTS_ITERATOR_H
|
||||
#include <core/playlists/playlist.h>
|
||||
|
||||
/* Called to set the playlist iterator to a specific position. */
|
||||
static inline playlist_iter playlist_iter_get(struct playlist *playlist,
|
||||
unsigned int n)
|
||||
{
|
||||
return playlist ? g_queue_peek_nth_link(&playlist->pl_tracks, n) : NULL;
|
||||
}
|
||||
|
||||
/* Called to advance the requested playlist iterator. */
|
||||
static inline playlist_iter playlist_iter_next(playlist_iter iter)
|
||||
{
|
||||
return g_list_next(iter);
|
||||
}
|
||||
|
||||
/* Called to get a pointer to the track at the requested iterator. */
|
||||
static inline struct track *playlist_iter_track(playlist_iter iter)
|
||||
{
|
||||
return iter ? iter->data : NULL;
|
||||
}
|
||||
|
||||
/* Called to find the playlist index of the requested iterator. */
|
||||
static inline int playlist_iter_index(struct playlist *playlist,
|
||||
playlist_iter iter)
|
||||
{
|
||||
return (playlist && iter) ? g_queue_link_index(&playlist->pl_tracks, iter) : -1;
|
||||
}
|
||||
|
||||
/* Called to iterate over the entire playlist. */
|
||||
#define playlist_for_each(playlist, it) \
|
||||
for (it = playlist_iter_get(playlist, 0); it; it = playlist_iter_next(it))
|
||||
|
||||
|
||||
/* Called to set the index of the current track. */
|
||||
static inline bool playlist_current_set(struct playlist *playlist,
|
||||
unsigned int n)
|
||||
{
|
||||
if (playlist)
|
||||
playlist->pl_current = playlist_iter_get(playlist, n);
|
||||
return playlist && playlist->pl_current;
|
||||
}
|
||||
|
||||
/* Called to advance the current track. */
|
||||
static inline bool playlist_current_next(struct playlist *playlist)
|
||||
{
|
||||
if (playlist)
|
||||
playlist->pl_current = playlist_iter_next(playlist->pl_current);
|
||||
return playlist && playlist->pl_current;
|
||||
}
|
||||
|
||||
/* Called to rewind the current track. */
|
||||
static inline bool playlist_current_previous(struct playlist *playlist)
|
||||
{
|
||||
if (playlist)
|
||||
playlist->pl_current = g_list_previous(playlist->pl_current);
|
||||
return playlist && playlist->pl_current;
|
||||
}
|
||||
|
||||
/* Called to get a pointer to the current track. */
|
||||
static inline struct track *playlist_current_track(struct playlist *playlist)
|
||||
{
|
||||
return playlist ? playlist_iter_track(playlist->pl_current) : NULL;
|
||||
}
|
||||
|
||||
/* Called to get the playlist index of the current track. */
|
||||
static inline int playlist_current_index(struct playlist *playlist)
|
||||
{
|
||||
return playlist ? playlist_iter_index(playlist, playlist->pl_current) : -1;
|
||||
}
|
||||
|
||||
/* Called to find the nth track on a playlist. */
|
||||
static inline struct track *playlist_at(struct playlist *playlist, unsigned int n)
|
||||
{
|
||||
return playlist_iter_track(playlist_iter_get(playlist, n));
|
||||
}
|
||||
#endif /* OCARINA_CORE_PLAYLISTS_ITERATOR_H */
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright 2016 (c) Anna Schumaker.
|
||||
*/
|
||||
#ifndef OCARINA_CORE_PLAYLISTS_LIBRARY_H
|
||||
#define OCARINA_CORE_PLAYLISTS_LIBRARY_H
|
||||
#include <core/playlists/generic.h>
|
||||
|
||||
/* Library playlist type. */
|
||||
extern struct playlist_type pl_library;
|
||||
|
||||
|
||||
/* Called to initialize library playlists. */
|
||||
void pl_library_init(void);
|
||||
|
||||
/* Called to deinitialize system playlists. */
|
||||
void pl_library_deinit();
|
||||
|
||||
/* Called to update a library path. */
|
||||
void pl_library_update(struct playlist *);
|
||||
|
||||
#endif /* OCARINA_CORE_PLAYLISTS_LIBRARY_H */
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* Copyright 2016 (c) Anna Schumaker.
|
||||
*/
|
||||
#ifndef OCARINA_CORE_PLAYLISTS_PLAYLIST_H
|
||||
#define OCARINA_CORE_PLAYLISTS_PLAYLIST_H
|
||||
#include <core/tags/track.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
typedef GList * playlist_iter;
|
||||
struct playlist;
|
||||
|
||||
enum playlist_type_t {
|
||||
PL_SYSTEM,
|
||||
PL_ARTIST,
|
||||
PL_LIBRARY,
|
||||
PL_USER,
|
||||
PL_MAX_TYPE,
|
||||
};
|
||||
|
||||
|
||||
#define PL_RANDOM (1 << 1)
|
||||
|
||||
|
||||
struct playlist_ops {
|
||||
/* Called to add a track to a playlist. */
|
||||
bool (*pl_add)(struct playlist *, struct track *);
|
||||
|
||||
/* Called to check if a playlist can be selected. */
|
||||
bool (*pl_can_select)(struct playlist *);
|
||||
|
||||
/* Called to delete a playlist. */
|
||||
bool (*pl_delete)(struct playlist *);
|
||||
|
||||
/* Called to remove a track from the playlist. */
|
||||
bool (*pl_remove)(struct playlist *, struct track *);
|
||||
|
||||
/* Called to set a playlist flag. */
|
||||
void (*pl_set_random)(struct playlist *, bool);
|
||||
|
||||
/* Called to sort the playlist. */
|
||||
void (*pl_sort)(struct playlist *, enum compare_t);
|
||||
|
||||
/* Called to rearrange the playlist. */
|
||||
bool (*pl_rearrange)(struct playlist *, unsigned int, unsigned int);
|
||||
};
|
||||
|
||||
|
||||
struct playlist {
|
||||
enum playlist_type_t pl_type; /* This playlist's type. */
|
||||
gchar *pl_name; /* This playlist's name. */
|
||||
unsigned int pl_id; /* This playlist's identifier. */
|
||||
|
||||
GQueue pl_tracks; /* This playlist's queue of tracks. */
|
||||
unsigned int pl_length; /* This playlist's length, in seconds. */
|
||||
bool pl_random; /* This playlist's random setting. */
|
||||
playlist_iter pl_current; /* This playlist's current track. */
|
||||
GSList *pl_sort; /* This playlist's sort order. */
|
||||
gchar **pl_search; /* This playlist's search text. */
|
||||
|
||||
const struct playlist_ops *pl_ops; /* This playlist's supported operations. */
|
||||
};
|
||||
|
||||
|
||||
#define DEFINE_PLAYLIST(type, name, id, ops) { \
|
||||
.pl_type = type, \
|
||||
.pl_name = name, \
|
||||
.pl_id = id, \
|
||||
.pl_ops = ops, \
|
||||
}
|
||||
|
||||
|
||||
struct playlist_type {
|
||||
/* Called to save all playlists of the given type. */
|
||||
void (*pl_save)(void);
|
||||
|
||||
/* Called to look up playlists. */
|
||||
struct playlist *(*pl_lookup)(const gchar *);
|
||||
struct playlist *(*pl_get)(unsigned int);
|
||||
|
||||
/* Called to create a new playlist. */
|
||||
struct playlist *(*pl_new)(const gchar *);
|
||||
|
||||
/* Called to notify that a track has been played. */
|
||||
void (*pl_played)(struct track *);
|
||||
|
||||
/* Called to notify that a track has been selected. */
|
||||
void (*pl_selected)(struct track *);
|
||||
};
|
||||
|
||||
|
||||
/* Called to check if the playlist contains a specific track. */
|
||||
static inline bool playlist_has(struct playlist *playlist, struct track *track)
|
||||
{
|
||||
return playlist ? g_queue_find(&playlist->pl_tracks, track) != NULL : false;
|
||||
}
|
||||
|
||||
/* Called to find the size of a playlist. */
|
||||
static inline unsigned int playlist_size(struct playlist *playlist)
|
||||
{
|
||||
return playlist ? g_queue_get_length(&playlist->pl_tracks) : 0;
|
||||
}
|
||||
|
||||
/* Called to clear the sort order of the playlist. */
|
||||
static inline void playlist_clear_sort(struct playlist *playlist)
|
||||
{
|
||||
if (playlist) {
|
||||
g_slist_free(playlist->pl_sort);
|
||||
playlist->pl_sort = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* OCARINA_CORE_PLAYLISTS_PLAYLIST_H */
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright 2016 (c) Anna Schumaker.
|
||||
*/
|
||||
#ifndef OCARINA_CORE_PLAYLISTS_SYSTEM_H
|
||||
#define OCARINA_CORE_PLAYLISTS_SYSTEM_H
|
||||
#include <core/playlists/generic.h>
|
||||
|
||||
enum sys_playlist_t {
|
||||
SYS_PL_FAVORITES, /* Songs that the user likes. */
|
||||
SYS_PL_HIDDEN, /* Songs that the user has hidden. */
|
||||
SYS_PL_QUEUED, /* Songs that the user has queued up. */
|
||||
SYS_PL_COLLECTION, /* Songs that have not been hidden. */
|
||||
SYS_PL_HISTORY, /* Songs that have just been played. */
|
||||
SYS_PL_UNPLAYED, /* Songs that have not been played yet. */
|
||||
SYS_PL_MOST_PLAYED, /* Songs with an above average play count. */
|
||||
SYS_PL_LEAST_PLAYED, /* Songs with a below average play count. */
|
||||
SYS_PL_NUM_PLAYLISTS, /* Number of system playlists. */
|
||||
};
|
||||
|
||||
|
||||
/* System playlist type. */
|
||||
extern struct playlist_type pl_system;
|
||||
|
||||
|
||||
/* Called to initialize system playlists. */
|
||||
void pl_system_init(void);
|
||||
|
||||
/* Called to deinitialize system playlists. */
|
||||
void pl_system_deinit();
|
||||
|
||||
|
||||
/* Called to tell system playlists about a new track. */
|
||||
void pl_system_new_track(struct track *);
|
||||
|
||||
/* Called to tell system playlists that a track is getting deleted. */
|
||||
void pl_system_delete_track(struct track *);
|
||||
|
||||
#endif /* OCARINA_CORE_PLAYLISTS_SYSTEM_H */
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright 2016 (c) Anna Schumaker.
|
||||
*/
|
||||
#ifndef OCARINA_CORE_PLAYLISTS_USER_H
|
||||
#define OCARINA_CORE_PLAYLISTS_USER_H
|
||||
#include <core/playlists/generic.h>
|
||||
|
||||
struct user_playlist {
|
||||
struct playlist pl_playlist;
|
||||
struct db_entry pl_dbe;
|
||||
};
|
||||
|
||||
#define USER_PLAYLIST(dbe) ((struct user_playlist *)DBE_DATA(dbe))
|
||||
|
||||
|
||||
/* User playlist type. */
|
||||
extern struct playlist_type pl_user;
|
||||
|
||||
|
||||
/* Called to initialize user playlists. */
|
||||
void pl_user_init(void);
|
||||
|
||||
/* Called to deinitialize user playlists. */
|
||||
void pl_user_deinit();
|
||||
|
||||
/* Called to tell user playlists that a track is getting deleted. */
|
||||
void pl_user_delete_track(struct track *);
|
||||
|
||||
/* Called to rename a user playlist. */
|
||||
bool pl_user_rename(struct playlist *, const gchar *);
|
||||
|
||||
struct database *pl_user_db_get();
|
||||
#endif /* OCARINA_CORE_PLAYLISTS_USER_H */
|
|
@ -1,118 +0,0 @@
|
|||
/*
|
||||
* Copyright 2013 (c) Anna Schumaker.
|
||||
*
|
||||
* Queues are lists of tracks that the user has requested to play next.
|
||||
* Users of queues are expected to implement their own save and load functions,
|
||||
* and to provide a filled out queue_ops structure during initialization.
|
||||
*/
|
||||
#ifndef OCARINA_CORE_QUEUE_H
|
||||
#define OCARINA_CORE_QUEUE_H
|
||||
|
||||
#include <core/containers/queue.h>
|
||||
#include <core/file.h>
|
||||
#include <core/tags/track.h>
|
||||
|
||||
struct queue;
|
||||
|
||||
|
||||
enum queue_flags {
|
||||
Q_ENABLED = (1 << 0), /* Queue is enabled. */
|
||||
Q_RANDOM = (1 << 1), /* Queue will pick songs randomly. */
|
||||
Q_REPEAT = (1 << 2), /* Queue will not remove songs when picked. */
|
||||
Q_NO_SORT = (1 << 3), /* Queue will not be sorted. */
|
||||
Q_SAVE_FLAGS = (1 << 4), /* Queue will be saved when flags change. */
|
||||
Q_SAVE_SORT = (1 << 5), /* Queue will be saved when sorted. */
|
||||
Q_ADD_FRONT = (1 << 6), /* Queue will add new tracks at the front. */
|
||||
};
|
||||
|
||||
|
||||
struct queue_ops {
|
||||
/* Called to tell a higher layer that a track has been added. */
|
||||
void (*qop_added)(struct queue *, unsigned int);
|
||||
|
||||
/* Called to tell a higher layer that a track has been removed. */
|
||||
void (*qop_removed)(struct queue *, unsigned int);
|
||||
|
||||
/* Called to tell a higher layer that the queue has been cleared. */
|
||||
void (*qop_cleared)(struct queue *, unsigned int);
|
||||
|
||||
/* Called to have a higher layer save the queue. */
|
||||
void (*qop_save)(struct queue *, enum queue_flags);
|
||||
|
||||
/* Called to hell a higher layer that a track has been updated. */
|
||||
void (*qop_updated)(struct queue *, unsigned int);
|
||||
};
|
||||
|
||||
|
||||
struct queue {
|
||||
unsigned int q_flags; /* The queue's set of flags. */
|
||||
unsigned int q_length; /* The queue's total runtime (in seconds). */
|
||||
struct _queue q_tracks; /* The queue's list of tracks. */
|
||||
struct _q_iter q_cur; /* The queue's last-played position. */
|
||||
GSList *q_sort; /* The queue's sort order. */
|
||||
|
||||
const struct queue_ops *q_ops; /* The queue's operations vector. */
|
||||
};
|
||||
|
||||
|
||||
/* Called to initialize a queue. */
|
||||
void queue_init(struct queue *, unsigned int, const struct queue_ops *);
|
||||
|
||||
/* Called to deinitialize a queue. */
|
||||
void queue_deinit(struct queue *);
|
||||
|
||||
|
||||
/* Called to set a queue flag. */
|
||||
void queue_set_flag(struct queue *, enum queue_flags);
|
||||
|
||||
/* Called to clear a queue flag. */
|
||||
void queue_unset_flag(struct queue *, enum queue_flags);
|
||||
|
||||
/* Called to check if the queue has a specific flag set. */
|
||||
static inline bool queue_has_flag(struct queue *queue, enum queue_flags flag)
|
||||
{
|
||||
return (queue->q_flags & flag) == (unsigned int)flag;
|
||||
}
|
||||
|
||||
/* Called to find the size of the queue. */
|
||||
static inline unsigned int queue_size(struct queue *queue)
|
||||
{
|
||||
return _q_size(&queue->q_tracks);
|
||||
}
|
||||
|
||||
/* Called to access the queued track at a given index. */
|
||||
static inline struct track *queue_at(struct queue *queue, unsigned int index)
|
||||
{
|
||||
return (struct track *)g_queue_peek_nth(&queue->q_tracks._queue, index);
|
||||
}
|
||||
|
||||
|
||||
/* Called to add a track to the queue. */
|
||||
unsigned int queue_add(struct queue *, struct track *);
|
||||
|
||||
/* Called to remove a track from the queue by index. */
|
||||
void queue_remove(struct queue *, unsigned int);
|
||||
|
||||
/* Called to remove all instances of the track from the queue. */
|
||||
void queue_remove_all(struct queue *, struct track *);
|
||||
|
||||
/* Called to remove all tracks from the queue. */
|
||||
void queue_clear(struct queue *);
|
||||
|
||||
/* Called to tell the queue that a track has been updated. */
|
||||
void queue_updated(struct queue *, struct track *);
|
||||
|
||||
|
||||
/* Called to tell the queue that a specific index has been selected. */
|
||||
void queue_selected(struct queue *, unsigned int);
|
||||
|
||||
/* Called to pick the next track from the queue. */
|
||||
struct track *queue_next(struct queue *);
|
||||
|
||||
/* Called to sort the queue without changing sort order. */
|
||||
void queue_resort(struct queue *);
|
||||
|
||||
/* Called to change the sort order and resort the queue. */
|
||||
void queue_sort(struct queue *, enum compare_t, bool);
|
||||
|
||||
#endif /* OCARINA_CORE_QUEUE_H */
|
|
@ -1,36 +0,0 @@
|
|||
/*
|
||||
* Copyright 2014 (c) Anna Schumaker
|
||||
*/
|
||||
#ifndef OCARINA_CORE_RANDOM_H
|
||||
#define OCARINA_CORE_RANDOM_H
|
||||
|
||||
#ifdef CONFIG_TESTING
|
||||
|
||||
void _seed_random(unsigned int);
|
||||
unsigned int _pick_random();
|
||||
|
||||
#else /* CONFIG_TESTING */
|
||||
|
||||
#include <stdlib.h>
|
||||
static inline void _seed_random(unsigned int n) { srand(time(NULL) + n); }
|
||||
static inline unsigned int _pick_random() { return rand(); }
|
||||
|
||||
#endif /* CONFIG_TESTING */
|
||||
|
||||
|
||||
|
||||
/* Seed the random number generator. */
|
||||
static inline void random_seed(unsigned int n)
|
||||
{
|
||||
_seed_random(n);
|
||||
}
|
||||
|
||||
/* Returns a random number X, where min <= value <= max. */
|
||||
static inline unsigned int random_range(unsigned int min, unsigned int max)
|
||||
{
|
||||
if (min >= max)
|
||||
return min;
|
||||
return min + (_pick_random() % (max - min));
|
||||
}
|
||||
|
||||
#endif /* OCARINA_CORE_RANDOM_H */
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright 2015 (c) Anna Schumaker.
|
||||
*
|
||||
* The settings layer is used to store values configured by the user.
|
||||
*/
|
||||
#ifndef OCARINA_SETTINGS_H
|
||||
#define OCARINA_SETTINGS_H
|
||||
#include <glib.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
|
||||
/* Called to initialize GUI settings. */
|
||||
void settings_init();
|
||||
|
||||
/* Called to deinitialize GUI settings. */
|
||||
void settings_deinit();
|
||||
|
||||
/* Called to configure a specific setting by key. */
|
||||
void settings_set(const gchar *, unsigned int);
|
||||
|
||||
/* Called to access a specific setting by key. */
|
||||
unsigned int settings_get(const gchar *);
|
||||
|
||||
/* Called to check if a specific settings exists. */
|
||||
bool settings_has(const gchar *);
|
||||
|
||||
|
||||
#ifdef CONFIG_TESTING
|
||||
GHashTable *test_get_settings();
|
||||
#endif /* CONFIG_TESTING */
|
||||
#endif /* OCARINA_SETTINGS_H */
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
#include <glib.h>
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
|
||||
/* Convert number of seconds into a string with format mm:ss. */
|
||||
|
@ -20,9 +21,6 @@ gchar *string_sec2str_long(unsigned int);
|
|||
/* Convert a struct tm to a locale-dependant date string. */
|
||||
gchar *string_tm2str(struct tm *);
|
||||
|
||||
/* Convert the input string to lowercase, dropping special characters. */
|
||||
gchar *string_lowercase(const gchar *);
|
||||
|
||||
/*
|
||||
* Compare two strings.
|
||||
*
|
||||
|
@ -30,6 +28,24 @@ gchar *string_lowercase(const gchar *);
|
|||
* if ret = 0: lhs == rhs.
|
||||
* if ret > 0: lhs > rhs, or lhs is empty.
|
||||
*/
|
||||
int string_compare(const gchar *, const gchar *);
|
||||
int string_compare_tokens(gchar **, gchar **);
|
||||
|
||||
/* Returns True if the two strings match. */
|
||||
static inline bool string_match(const gchar *a, const gchar *b)
|
||||
{
|
||||
return (a && b) ? g_utf8_collate(a, b) == 0 : false;
|
||||
}
|
||||
|
||||
/* Returns True if one of the tokens begins with the specified prefix. */
|
||||
bool string_match_token(const gchar *, gchar **);
|
||||
|
||||
/* Returns True if string a is a subdirectory of string b. */
|
||||
bool string_is_subdir(const gchar *, const gchar *);
|
||||
|
||||
/* Return the length of the string, with NULL checks */
|
||||
static inline int string_length(const gchar *str)
|
||||
{
|
||||
return str ? strlen(str) : 0;
|
||||
}
|
||||
|
||||
#endif /* OCARINA_CORE_STRING_H */
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue