Comparar commits
810 Commits
| Autor | SHA1 | Data | |
|---|---|---|---|
| f8562992d9 | |||
| ee1ac265bd | |||
| 6efdf8b750 | |||
| 0d2fcc27bb | |||
| 43f2dec84f | |||
| d5768b8184 | |||
| ab4888efca | |||
| 5847ab0081 | |||
| 7a4b510a2a | |||
| 9b188f4d4e | |||
| ae918845ff | |||
| ed275b8736 | |||
| e377735696 | |||
| 0979b84e85 | |||
| b4cd3aa305 | |||
| d266d851ac | |||
| cb76fc11ff | |||
| 8e4ff492f2 | |||
| 20440530e2 | |||
| 5fde6b4db4 | |||
| e5b3ac585f | |||
| 13007fa6af | |||
| 3baac6bf26 | |||
| 6d995200f9 | |||
| d9e952bcba | |||
| 4c0f10b950 | |||
| b77391b13d | |||
| 5d30d526fc | |||
| a199f1019e | |||
| b6e0295968 | |||
| d9c310bdf9 | |||
| fd69e2fbaa | |||
| 45e59f82d5 | |||
| 08c0ad0991 | |||
| 58339c78f7 | |||
| 3000ce1dd2 | |||
| fa5833a969 | |||
| 1a83844fe0 | |||
| 5a946e87e5 | |||
| 9a892d0a67 | |||
| 01534acefa | |||
| a51c43fc0d | |||
| 0a3d4be746 | |||
| f6a7119247 | |||
| 850f9bcb9b | |||
| 92995493d0 | |||
| 4bc283a420 | |||
| 124f6f647d | |||
| a49ec03b67 | |||
| bdf8a7638a | |||
| b268362880 | |||
| 35ce340691 | |||
| 444cc7cd58 | |||
| 5ce1e3edd5 | |||
| 53a5b0bc3d | |||
| b01a20d806 | |||
| ceea5d06dc | |||
| 9425b1e1b5 | |||
| 72fc8cb4b2 | |||
| 8880d9a08e | |||
| a51329e0dd | |||
| 4ffb187890 | |||
| 0256a65b18 | |||
| cbbb254886 | |||
| d24459f6a4 | |||
| 2dfb1a1566 | |||
| 8e53da5ca1 | |||
| 746e6b9ede | |||
| d3bbfa798a | |||
| 908b564cb8 | |||
| ffa85938a2 | |||
| c634c8f514 | |||
| d7eea33c9d | |||
| 6a02d3b74a | |||
| 92020328ea | |||
| 5ad55ccab3 | |||
| 16d2593b09 | |||
| 47e5220357 | |||
| aca7ff9d02 | |||
| 3abf4537db | |||
| 666395e547 | |||
| 533c830838 | |||
| 1900e51ff1 | |||
| 513f9301dc | |||
| 3aa9e24f81 | |||
| 58eed3a889 | |||
| 4058056b70 | |||
| a9fe7ddd27 | |||
| ac52161047 | |||
| e2803df857 | |||
| 8aa2e7105e | |||
| f968221bff | |||
| 90b4bf31f3 | |||
| 49b355b8cf | |||
| edfd24bbd3 | |||
| c5042bdc57 | |||
| e6c650d7b8 | |||
| 52e3e7e2ef | |||
| 82558792b5 | |||
| e0e5e5b812 | |||
| 31041fa39a | |||
| 1dcbc4ce07 | |||
| 003b10afa6 | |||
| c1fbb27b0c | |||
| 657abe17fb | |||
| ba5d9e139a | |||
| d345e75952 | |||
| 0f20559e2f | |||
| 5977335253 | |||
| 6d5be5812a | |||
| 5c20b3bdcc | |||
| 17c871897e | |||
| 8947aebe48 | |||
| 802cd5a36a | |||
| 115a652a71 | |||
| 2148fd428a | |||
| 9c90b0b8bb | |||
| e10a75945a | |||
| b1837365a2 | |||
| d9dc24447c | |||
| b35f4b5615 | |||
| ad04ae14dd | |||
| 8392648ffe | |||
| fd07433c87 | |||
| a78ad58de5 | |||
| 052df42b29 | |||
| cd3847ba67 | |||
| cabc112d1c | |||
| cfde5a42b4 | |||
| 72710425e5 | |||
| 606bb0d065 | |||
| 7ec7ff72e3 | |||
| b6dd370470 | |||
| 5b23026ae8 | |||
| e71656aeb3 | |||
| 67b5575fb4 | |||
| 0a71517161 | |||
| 5735d6d6f3 | |||
| c98881b676 | |||
| 7806c91c86 | |||
| a34ebbd3db | |||
| 635afa73e5 | |||
| ee5b1a19ea | |||
| cb3b67991b | |||
| 1504c90b4d | |||
| 9a8efff960 | |||
| 9068149d07 | |||
| 4decb6c260 | |||
| 8303145003 | |||
| 039a772795 | |||
| 84f34bcf18 | |||
| c766a4dedc | |||
| 4fe43a950f | |||
| 437dc66cf7 | |||
| d4e172aafb | |||
| 28a8c346c7 | |||
| a4c8d23fbb | |||
| d35f173453 | |||
| 3a9589074f | |||
| 8cf80c99bd | |||
| 0bbde2077c | |||
| 519fb78c84 | |||
| 2d828b4587 | |||
| 0dd34ca8f3 | |||
| 2d6bcd02b6 | |||
| 5fa4fd391e | |||
| 535059df62 | |||
| 6f10706ac1 | |||
| a5a3d6b85c | |||
| cae74bf6e7 | |||
| 08b979fa0a | |||
| 7a20522c2f | |||
| 8243b2ed34 | |||
| 1ea5d3dd67 | |||
| 4c2ef51454 | |||
| c7696d412c | |||
| 82adb4653d | |||
| 41e80ee47b | |||
| 87ea90d054 | |||
| ec24905ed0 | |||
| 1758a56ef2 | |||
| 934606eb2f | |||
| 043790eda2 | |||
| eed81c45a1 | |||
| edf995395a | |||
| e12d09e52e | |||
| 0ec0cd8f75 | |||
| 473b656412 | |||
| 22c0b68add | |||
| 8edf61af6a | |||
| e8aad1c034 | |||
| 4091c9be50 | |||
| d2be6895f0 | |||
| 52b8588f3f | |||
| e7669d3117 | |||
| 1804a55426 | |||
| b514740e19 | |||
| d39e964bc5 | |||
| c7f6281e11 | |||
| 4afc22c686 | |||
| fb64f74663 | |||
| 3896c62844 | |||
| 52033cb0a9 | |||
| 48c21be900 | |||
| 72a903992d | |||
| f8351aadb4 | |||
| 29da8eda3e | |||
| b202684e0a | |||
| 873571f1ab | |||
| 48342cdd18 | |||
| 90e02345fb | |||
| b48731ed6a | |||
| 870b4cdd7b | |||
| ecfdc66706 | |||
| aa8d23e65b | |||
| 5013de9127 | |||
| 0c0d4d6c57 | |||
| 9da85017b9 | |||
| 4174f71436 | |||
| 3088768fe6 | |||
| acb6caaba0 | |||
| 8619d73007 | |||
| 18666de36f | |||
| e9af41d634 | |||
| 9763743a95 | |||
| b6ffa2de70 | |||
| 74adbe6c3f | |||
| fd42d4e601 | |||
| 0b97a39edc | |||
| 23e2ed5297 | |||
| 273841ec65 | |||
| d128149d8d | |||
| 9cdf49e975 | |||
| 74319826d2 | |||
| afea82eee5 | |||
| dda323ac04 | |||
| a09a25690c | |||
| f27ee2464c | |||
| 18aaf08f4b | |||
| c03166007f | |||
| 0c66334569 | |||
| 1e88bab104 | |||
| b2eabeb8eb | |||
| d93ea16d1a | |||
| ac321f7815 | |||
| c53e6c33f8 | |||
| 368dda444b | |||
| d2640cf11a | |||
| b56ec0e5d9 | |||
| 5d0d513f93 | |||
| 914098f4f4 | |||
| 16beb62e9c | |||
| c1eb1d279e | |||
| f9ab0104cd | |||
| 1a7e154f64 | |||
| 4cfe692b9e | |||
| d165f5fc0c | |||
| 06a7478c70 | |||
| 7d629e5bcb | |||
| 5ddf0b306c | |||
| ade9939053 | |||
| e6f8c887c9 | |||
| db09860d08 | |||
| 1718296e46 | |||
| f5a6b56c3a | |||
| 53ff1990d2 | |||
| 4e5929237f | |||
| f1f4f451b8 | |||
| 581c1b6c0c | |||
| 2cafad60b6 | |||
| 1cd75c4664 | |||
| 161997c0d0 | |||
| cc32904e80 | |||
| c0edd4895e | |||
| f9dfa0c39d | |||
| 7eaeebf168 | |||
| 290f06981e | |||
| 87de8b9a8b | |||
| 7a233c2dbb | |||
| 931135ba70 | |||
| adba91d077 | |||
| 466112068a | |||
| 767e2791cf | |||
| f60bf048b2 | |||
| 006d621327 | |||
| 03b01b58f2 | |||
| e915b353cc | |||
| cf8e2b383e | |||
| 272127f5a3 | |||
| d4f0e9212a | |||
| 78546f3643 | |||
| e1cd035095 | |||
| 07aca0cdf6 | |||
| 7b96b47a6c | |||
| d3654e1457 | |||
| 98045cae32 | |||
| 50ae5a2a99 | |||
| 0ca674fb7b | |||
| 0ed6f0e3ec | |||
| ed1d8899be | |||
| 0c8d408688 | |||
| d67a802b3d | |||
| 51c05b6202 | |||
| 2a098b1047 | |||
| 5effd50acd | |||
| 729d7f1ff0 | |||
| 47faf9a509 | |||
| 6afe9bbed5 | |||
| 6bb5f885bb | |||
| f763b00f3a | |||
| 7d903e84cf | |||
| 7cbf51d168 | |||
| 4a1443cf66 | |||
| 7a45d9531e | |||
| b1544a8983 | |||
| 1ff243df66 | |||
| 9076627ca9 | |||
| 49d9540f8c | |||
| 5d1e68c268 | |||
| 4692c6b8ee | |||
| f34fd79952 | |||
| 295ee80351 | |||
| a508e52f0d | |||
| 2bda0f1cb0 | |||
| 74f37075e5 | |||
| 88821ef68a | |||
| 677855e5c7 | |||
| d593463a25 | |||
| 66070a6b0e | |||
| 66b4895458 | |||
| 2f6af4fa7a | |||
| 80eefa8737 | |||
| 5fa51b6ed3 | |||
| 4bdd1b4809 | |||
| 0c65a494d6 | |||
| f0acad9dca | |||
| 10b4c152c4 | |||
| 1eab887896 | |||
| 616ccb2d53 | |||
| 8b5e234cd2 | |||
| dfc0002a2c | |||
| 111147ca2a | |||
| 297378349c | |||
| 0678179585 | |||
| a03119ef29 | |||
| 66ddba7d78 | |||
| 4a23b78816 | |||
| 07273d8351 | |||
| 1dc678f31f | |||
| 64f7f3a978 | |||
| 846e995d82 | |||
| 840ef0fd68 | |||
| c562d3c4b3 | |||
| b2aec88ccf | |||
| 7302165293 | |||
| 9502bcf528 | |||
| afbc8bd3b9 | |||
| 958e6ad6de | |||
| 7027112b58 | |||
| 2c8554d79f | |||
| 16f6055488 | |||
| d09ac59971 | |||
| 9e83e420bc | |||
| 7e2eb1b8f9 | |||
| d7b7de854a | |||
| 025cd52e3c | |||
| ab7643d7fa | |||
| 211f4019bb | |||
| 5ccd07e4b2 | |||
| 7b2c529228 | |||
| 2a28592360 | |||
| 00e0c80594 | |||
| a79591f9c0 | |||
| f32713855f | |||
| 27fdc07f28 | |||
| 8ba870184d | |||
| bd9f7c52c0 | |||
| 3fd7fa281e | |||
| cafda03761 | |||
| 752357ad68 | |||
| 1394cedbd2 | |||
| 5bc4d5ef31 | |||
| 97ec1dab0a | |||
| d0e3f218e0 | |||
| e37655d7e0 | |||
| d8967fb4e1 | |||
| b7a5a8e023 | |||
| 57808278d3 | |||
| 9412ddfb29 | |||
| d99cab53d1 | |||
| 9312c37305 | |||
| f8eb618d84 | |||
| 3f610a74a1 | |||
| 12826cdde6 | |||
| c6eb082aca | |||
| 58815e03a9 | |||
| 1380515c63 | |||
| 3c18ca9052 | |||
| f9422d8a19 | |||
| d64f1bcaf7 | |||
| 27c386e172 | |||
| 5e2cec619e | |||
| 8ab66a192f | |||
| 4e480c40fe | |||
| 9b58df3784 | |||
| 34583e0086 | |||
| 368e582fe2 | |||
| 186977f6ef | |||
| ce119120b7 | |||
| 6759472a2e | |||
| 58ef685380 | |||
| 594feaca93 | |||
| edcf74f005 | |||
| a6993e3d2e | |||
| 5ccc4c1c7d | |||
| 35335da6b8 | |||
| ba9ae93d87 | |||
| 6b999b0099 | |||
| 388e8faec6 | |||
| da92ed7d9a | |||
| a17a32e33b | |||
| 17db0b98df | |||
| d500fdb0bf | |||
| 69fccf10f4 | |||
| fa2050486e | |||
| 3f85bc1da6 | |||
| 9d59cb0748 | |||
| e1aa1ba082 | |||
| 8ac60117ac | |||
| 7179b2e601 | |||
| e93a7dd36b | |||
| 85df2fa79d | |||
| 3e55c8daa9 | |||
| 59f1a840ec | |||
| 74d6a62238 | |||
| ef657da311 | |||
| 250eea8053 | |||
| f243e2aba2 | |||
| e47c3dedac | |||
| 2a2a51c1ed | |||
| c27ae05e3c | |||
| 00ccb84fc3 | |||
| 83e73f95d5 | |||
| 154e5df152 | |||
| 5781a67a70 | |||
| 0322dfd248 | |||
| ecd1a07026 | |||
| 3273c920ec | |||
| 7b33009e67 | |||
| 73e218e99f | |||
| 30a6ebeb45 | |||
| 2e27bbd794 | |||
| a029c43144 | |||
| 474fba83c0 | |||
| 491d35b581 | |||
| e7b68f91b7 | |||
| c348988325 | |||
| 6c76820891 | |||
| fadb361a0c | |||
| 52d6828480 | |||
| 9d0666144b | |||
| c9d27129b1 | |||
| 5fc8e853ad | |||
| 0c072028c0 | |||
| bc3d194920 | |||
| da37365b38 | |||
| ebfc12ef15 | |||
| 017312e7c5 | |||
| 5302cca0a1 | |||
| b9f82a794f | |||
| b9ac6a5c8a | |||
| 1e1e6481a9 | |||
| 6840a52d67 | |||
| 42fb3f650b | |||
| 9130f32038 | |||
| 6a7b7f7146 | |||
| f4f1086883 | |||
| 14b84f1a9c | |||
| 44cf1a94c6 | |||
| e348ec9b45 | |||
| 0ea4d413c3 | |||
| 69800ebb6d | |||
| e9c869e5ad | |||
| 34367b8e85 | |||
| 77436aca08 | |||
| d72cac5424 | |||
| 10d26bb4d3 | |||
| f9cff865bf | |||
| 0e43b68a73 | |||
| 0541665488 | |||
| 76a9610422 | |||
| 6c09a042f9 | |||
| 844a514ccc | |||
| b991fd6993 | |||
| ab47c5f97d | |||
| 6a5eace53d | |||
| 639429fed1 | |||
| 28ee9474f8 | |||
| b98ea85bdb | |||
| bb02cbc035 | |||
| 3ef9bb6afc | |||
| b3ed080e09 | |||
| eb5014483a | |||
| 8bd4799bf4 | |||
| a3c3631ecb | |||
| e4ecbb3c11 | |||
| 94f7e1045e | |||
| a5a9096732 | |||
| 1f3f5b7df1 | |||
| da09955aeb | |||
| 1fce749c3e | |||
| 1bb5e94b1e | |||
| f19a5d3e75 | |||
| e479bb2983 | |||
| 2d61b6ebab | |||
| 339b25633a | |||
| a651ac93d4 | |||
| 44a281ff99 | |||
| 64b900f21a | |||
| dd84f9183c | |||
| acb86fea37 | |||
| dcc89ed8a2 | |||
| 100a552d56 | |||
| f96bc30716 | |||
| 727165a9d5 | |||
| b6bb7eaeb6 | |||
| 6002454340 | |||
| 023ebcf1d3 | |||
| 4a3add381b | |||
| 206f574cdf | |||
| 9789b697f0 | |||
| 0116413aa2 | |||
| 2b03440adc | |||
| d22355d29d | |||
| 1ccde3b5d4 | |||
| a10dc87ef0 | |||
| 828787dc8d | |||
| bc7052f671 | |||
| afc4a2939c | |||
| 5bad5fa5e2 | |||
| 452de7f834 | |||
| 205fa02435 | |||
| 791787559b | |||
| 70575fb308 | |||
| 7360482c9d | |||
| d05231de82 | |||
| 8b38ae236e | |||
| 0ed9d54e22 | |||
| e7d7efaf61 | |||
| 4cac3ed47d | |||
| 777e78fbc4 | |||
| e5e66a1f24 | |||
| 6aaee47cb6 | |||
| db028fa7c8 | |||
| 2f50a382ea | |||
| 60c6cba7cf | |||
| 424f18799d | |||
| b12423ca63 | |||
| f6c23576a0 | |||
| 5c1736cb04 | |||
| ebe6678e43 | |||
| 6ab0bcecd3 | |||
| b0b6a09100 | |||
| 97e1f79ad5 | |||
| 302048e93b | |||
| 157cc0359b | |||
| 85b76af44a | |||
| 6f29b47576 | |||
| 45aa5281e2 | |||
| 6f4ff35547 | |||
| b578d8f386 | |||
| 8d40dad422 | |||
| c7d6dc3481 | |||
| 953a159018 | |||
| 68aacc4f72 | |||
| 1dad8ae6d3 | |||
| 6860b4d4a3 | |||
| 28e6ad9a04 | |||
| c2260d6806 | |||
| 6ffdc361b3 | |||
| 4e8a081cad | |||
| 193bd37572 | |||
| e666803e2d | |||
| d6128831c4 | |||
| 3277635a97 | |||
| cd1a6d1f91 | |||
| ce452dbba5 | |||
| cf0b264062 | |||
| e95e4a0af7 | |||
| c851f3d913 | |||
| 83168a14bd | |||
| b792a42371 | |||
| dc0b9a113b | |||
| 77a3867152 | |||
| 6fb90792af | |||
| dabb80dc65 | |||
| 8855b85653 | |||
| ccff276ab0 | |||
| 8f42a9ea71 | |||
| c094a8f445 | |||
| ae44b76a41 | |||
| 263df5c62c | |||
| c7f8fe0ded | |||
| 3bd947bf16 | |||
| 65de81ebc0 | |||
| a4c8cc4258 | |||
| bc59cd85a2 | |||
| 08dc4ee2e1 | |||
| c098d0a5ac | |||
| 419b95d49c | |||
| 8f3c66b726 | |||
| 35b2bda943 | |||
| 7c584aa1e3 | |||
| d1583f78f5 | |||
| fd4f1a86fd | |||
| 8d58c448e8 | |||
| 83f0194f2f | |||
| 6347abcd2f | |||
| 09c73972a5 | |||
| e9f16ab6b3 | |||
| 0a012d97bc | |||
| d674618904 | |||
| 9349e965c3 | |||
| 0a6dc5620b | |||
| f78c77c98d | |||
| 2c025d32f8 | |||
| aa6144d575 | |||
| 37f1cfd632 | |||
| c38bdf0608 | |||
| 2550d46f15 | |||
| c868c1a44e | |||
| 9aa78d4c6a | |||
| 059f8a937b | |||
| 204ff93a59 | |||
| f6d83086c4 | |||
| a100ed29da | |||
| 555d6490b5 | |||
| ae6042e228 | |||
| aa44101ed5 | |||
| c6099bb89d | |||
| 86b25086ac | |||
| 70e8139b48 | |||
| c0a5d2566f | |||
| 9f4b104322 | |||
| ab60fb0eb3 | |||
| 292201f926 | |||
| 522cdb25f8 | |||
| 362abfbedc | |||
| 2be1cf05b1 | |||
| 82777c340c | |||
| 207c3ee97d | |||
| 6a5ef715e5 | |||
| 8be7155fd0 | |||
| a395c00df8 | |||
| e0ef6f040d | |||
| 3d41829949 | |||
| 9c142c6036 | |||
| 6cb11029b1 | |||
| 449d0375de | |||
| e880bba103 | |||
| c4504c274d | |||
| 0c70ecdb6b | |||
| edef1f564e | |||
| 33820cfeb9 | |||
| 50b7c1f936 | |||
| 8c756fa01a | |||
| acb5ff7061 | |||
| 6af7e36af8 | |||
| bf56c47c3e | |||
| f3f389006a | |||
| 8f18571626 | |||
| ae8f3a5da3 | |||
| 8fcf0f6712 | |||
| 9ce6e8c7d9 | |||
| aa4904ded7 | |||
| da31e95cab | |||
| 198426e55e | |||
| ef2ff02b28 | |||
| 3744855057 | |||
| f296c644a6 | |||
| cd665e2bb9 | |||
| 35c02421a5 | |||
| dcc3dc3187 | |||
| 75b9a540fe | |||
| 1d639d6730 | |||
| ca1fbcd899 | |||
| 25940e3485 | |||
| 2438cbdd08 | |||
| bab9311225 | |||
| 76215de1b2 | |||
| 3c6a3e8c4d | |||
| eddfe978a3 | |||
| a38fc28763 | |||
| 9bf3f333ea | |||
| 6d2d45b518 | |||
| 408f78d634 | |||
| ced468ac65 | |||
| de9bdf67e3 | |||
| 8859688606 | |||
| ccd8f10786 | |||
| fe159d07b7 | |||
| 4057318255 | |||
| 3f47a48f71 | |||
| 2d24ca1d38 | |||
| 039330dbae | |||
| 0590acc605 | |||
| 2a881319e3 | |||
| fabe82c919 | |||
| 0a32a19aec | |||
| 62ac5f5a15 | |||
| 9889e2ecb8 | |||
| d3e9961a56 | |||
| c8624ea86f | |||
| 6e2bd1f650 | |||
| dbdadac043 | |||
| cd9d36f688 | |||
| 7993695a11 | |||
| ac861a82b5 | |||
| 69c5979fb6 | |||
| b1d598b762 | |||
| 0db85fe051 | |||
| 6dbce1846e | |||
| f336b2f64e | |||
| d6028ca00c | |||
| db8123d960 | |||
| 6845ebc3b9 | |||
| 71cc9976de | |||
| 97634dddd5 | |||
| 7ead18fc5d | |||
| 3fbc91fb57 | |||
| 8c8cfcdc6d | |||
| dc9d0332c3 | |||
| a63c513423 | |||
| 334976832b | |||
| ff5ccf49f7 | |||
| 5827b32e54 | |||
| 677fb805a9 | |||
| 7de5cb2940 | |||
| 94e639ef6c | |||
| b7a6347a5f | |||
| 403d1f51bb | |||
| 28aae8aad1 | |||
| f6dc74e7d2 | |||
| 4c6b3b12ab | |||
| a1eeaaa037 | |||
| 3ce9a8dc85 | |||
| c9fd8cf418 | |||
| e498d3dd92 | |||
| 3e19f0555a | |||
| 8d56ad73a8 | |||
| 01e3762c86 | |||
| c99eff7af7 | |||
| e5e3d935ae | |||
| 2853dc507d | |||
| 02231061d6 | |||
| 594a399b0c | |||
| 6ca4a5d3a2 | |||
| 41d3cd8cfc | |||
| 650c2320dc | |||
| f2f4e75670 | |||
| 42ddff79e1 | |||
| 30a8c8322e | |||
| 01c2f57f3b | |||
| 8942dac647 | |||
| 7888186e0c | |||
| 161eb5714b | |||
| 20388f54a7 | |||
| b5d9a53d44 | |||
| b00e3e6949 | |||
| 61b7c6386e | |||
| e1a68c4acb | |||
| 4f0251902a | |||
| f31dc04af1 | |||
| dd58b8a5e3 | |||
| d172061280 | |||
| 01696eaa2d | |||
| 82859ceb2f | |||
| 79bf0a5f79 | |||
| d382e30c2f | |||
| c24fd22f45 | |||
| 6f5783f8ba | |||
| 86e7c4a99f | |||
| 2a3d455838 | |||
| 96e425aee9 | |||
| 47ff73ba09 | |||
| 1097077104 | |||
| 18ba48f572 | |||
| 448663cfef | |||
| b52b7ec8dd | |||
| 83e587405f | |||
| 401ae1bd60 | |||
| cdd681a8e5 | |||
| 3079af48bc | |||
| 67f22db97a | |||
| b0a87d9d53 | |||
| 16be637fd0 | |||
| 784de34081 | |||
| 82545062b2 | |||
| 9c6d37a285 | |||
| 37da774bd6 | |||
| a6e3a42fab | |||
| 028a38570a | |||
| 718d9b6b61 | |||
| 4229614129 | |||
| c08be9f2fc | |||
| 623894d5a6 | |||
| b2485b739a | |||
| ebb93e6098 | |||
| 440ff178b6 | |||
| 0e4d722f2d |
@@ -7,6 +7,7 @@ dist
|
||||
build
|
||||
eggs
|
||||
parts
|
||||
.idea
|
||||
bin
|
||||
var
|
||||
sdist
|
||||
@@ -25,3 +26,5 @@ pip-log.txt
|
||||
|
||||
#Mr Developer
|
||||
.mr.developer.cfg
|
||||
|
||||
img/usericons/popups.xml
|
||||
|
||||
@@ -0,0 +1,621 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. {http://fsf.org/}
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
|
||||
@@ -13,17 +13,40 @@ clean:
|
||||
install:
|
||||
install -d $(DESTDIR)$(SUBDIR)
|
||||
install -m 644 *.py $(DESTDIR)$(SUBDIR)
|
||||
install -m 644 LICENSE.txt $(DESTDIR)$(SUBDIR)
|
||||
install -d $(DESTDIR)$(DATADIR)img
|
||||
install -m 644 img/*.png $(DESTDIR)$(DATADIR)img/
|
||||
install -m 644 img/*.svg $(DESTDIR)$(DATADIR)img/
|
||||
install -m 644 img/*.xml $(DESTDIR)$(DATADIR)img/
|
||||
install -d $(DESTDIR)$(DATADIR)img/darker
|
||||
install -m 644 img/darker/*.png $(DESTDIR)$(DATADIR)img/darker
|
||||
install -d $(DESTDIR)$(DATADIR)img/dark
|
||||
install -m 644 img/dark/*.png $(DESTDIR)$(DATADIR)img/dark
|
||||
install -d $(DESTDIR)$(DATADIR)img/light
|
||||
install -m 644 img/light/*.png $(DESTDIR)$(DATADIR)img/light
|
||||
install -d $(DESTDIR)$(DATADIR)img/lighter
|
||||
install -m 644 img/lighter/*.png $(DESTDIR)$(DATADIR)img/lighter
|
||||
install -d $(DESTDIR)$(DATADIR)img/standard
|
||||
install -m 644 img/standard/*.png $(DESTDIR)$(DATADIR)img/standard
|
||||
install -d $(DESTDIR)$(DATADIR)img/links
|
||||
install -m 644 img/links/*.png $(DESTDIR)$(DATADIR)img/links
|
||||
install -d $(DESTDIR)$(DATADIR)template
|
||||
install -m 644 template/*.xml $(DESTDIR)$(DATADIR)template
|
||||
install -d $(DESTDIR)$(DATADIR)ui
|
||||
install -m 644 ui/*.ui $(DESTDIR)$(DATADIR)ui/
|
||||
install -m 644 ui/*.xml $(DESTDIR)$(DATADIR)ui/
|
||||
install -m 644 coverart_browser.plugin $(DESTDIR)$(SUBDIR)
|
||||
install -m 644 coverart_search_providers.plugin $(DESTDIR)$(SUBDIR)
|
||||
install -m 644 ui/*.css $(DESTDIR)$(DATADIR)ui/
|
||||
install -d $(DESTDIR)$(DATADIR)coverflow
|
||||
install -d $(DESTDIR)$(DATADIR)coverflow/img
|
||||
install -m 644 coverflow/*.css $(DESTDIR)$(DATADIR)coverflow/
|
||||
install -m 644 coverflow/*.js $(DESTDIR)$(DATADIR)coverflow/
|
||||
install -m 644 coverflow/*.html $(DESTDIR)$(DATADIR)coverflow/
|
||||
install -m 644 coverflow/LICENSE $(DESTDIR)$(DATADIR)coverflow/
|
||||
install -m 644 coverflow/img/* $(DESTDIR)$(DATADIR)coverflow/img/
|
||||
install -m 644 coverart_browser.plugin* $(DESTDIR)$(SUBDIR)
|
||||
install -d $(DESTDIR)$(DATADIR)tmpl
|
||||
install -m 644 tmpl/* $(DESTDIR)$(DATADIR)tmpl/
|
||||
install -d $(DESTDIR)$(GLIB_DIR)
|
||||
install -m 644 schema/$(GLIB_SCHEME) $(DESTDIR)$(GLIB_DIR)
|
||||
cd po;./lang.sh $(DESTDIR)$(LOCALEDIR)
|
||||
cd po;./install_all.sh $(DESTDIR)$(LOCALEDIR)
|
||||
|
||||
|
||||
@@ -1,137 +1,119 @@
|
||||
coverart-browser v0.7
|
||||
================
|
||||
#coverart-browser - v2.2 development (Colonel K)
|
||||
|
||||
Browse your coverart albums in Rhythmbox v2.96 and later
|
||||
Browse your coverart albums in Rhythmbox v3 and later.
|
||||
|
||||

|
||||
If you have reached here looking for the stable version of the plugin please read the README files for
|
||||
|
||||
- rhythmbox 2.96 - 2.99: https://github.com/fossfreedom/coverart-browser/tree/release-1.2
|
||||
- rhythmbox 3.0+: https://github.com/fossfreedom/coverart-browser/tree/release-2.1
|
||||
|
||||

|
||||
|
||||
-----------
|
||||
|
||||
**Please help out with translating - skip to the end for details**
|
||||
##Authors
|
||||
|
||||
Summary: whats new in this release
|
||||
- asermax <asermax@gmail.com>, website - https://github.com/asermax
|
||||
|
||||
- find & display embedded covers in MP3, M4A, FLAC & Ogg files
|
||||
- Optional coverart search from Discogs internet service
|
||||
- new filter by decade
|
||||
- configurable shadow effect behind cover display
|
||||
- Iconised filter & sort buttons that change icon depending upon option chosen
|
||||
- popup menu selection for filters
|
||||
- brand new icons designed explicitly to the coverart browser plugin
|
||||
- one click open and close track view for a cover
|
||||
- plugin translated into many more languages
|
||||
- plugin code refactored - much faster to start and display without any flashing effects
|
||||
- code has been completely documented using doxygen: http://fossfreedom.github.com/coverart-browser/classes.html
|
||||
- covers displayed using rhythmbox natural sort i.e. ascending numbers
|
||||
- search filter matches upper & lower-case as well as ignoring characters such as accents
|
||||
- display covers for play-queue/music library & playlists from within the coverart-view
|
||||
- column header sort in track view
|
||||
- revamped plugin preferences
|
||||
[](http://flattr.com/thing/1262052/asermax-on-GitHub "asermax")
|
||||
|
||||
- fossfreedom <foss.freedom@gmail.com>, website - https://github.com/fossfreedom
|
||||
|
||||
[](http://flattr.com/thing/1811704/ "fossfreedom") [](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=KBV682WJ3BDGL)
|
||||
-----------
|
||||
|
||||
##Summary: whats new in this release
|
||||
|
||||
- Support for Alternative-Toolbar & Headerbar - add view switcher to headerbar and remove from source toolbars
|
||||
- Support for Alternative-Toolbar - Toggle between views and sources in sidepane
|
||||
- Export and Embed dialog remembers field values between openings
|
||||
- Quicker startup of plugin
|
||||
- chosen playlist are persistent between rhythmbox sessions
|
||||
- Add CoverArt Playlist to the same menu-button as other views
|
||||
- Remove separate CoverArt Playlist icon on track-view
|
||||
- Rework CoverArt Playlist so that don't have to start CoverArt to play the last saved playlist
|
||||
- various icons display correctly for both light and dark themes
|
||||
- Translated into 25 languages and locales
|
||||
- for developers - doxygen documentation: http://fossfreedom.github.io/coverart-browser/classes.html
|
||||
|
||||
*How it works:*
|
||||
|
||||
- Click the new CoverArt source button (left hand side of screen)
|
||||
- Click the new CoverArt Browser source button (left hand side of screen)
|
||||
- Albums are displayed as clickable buttons containing their album cover
|
||||
- Right click menu option to play, queue & search for coverart for an album.
|
||||
- Download Album & artist artwork via the properties toolbar button
|
||||
|
||||
- https://github.com/fossfreedom/coverart-browser/wiki/how-to-for-version-2.0
|
||||
- https://github.com/fossfreedom/coverart-browser/wiki/Screenshots
|
||||
|
||||
- https://github.com/fossfreedom/coverart-browser/wiki/How-the-plugin-works
|
||||
*How to install - Rhythmbox 3.0 and later:*
|
||||
|
||||
*How to install:*
|
||||
N.B. for earlier Rhythmbox versions use version 1.x
|
||||
|
||||
for debian & debian-based distros such as Ubuntu & Mint
|
||||
Prerequisite is to use a distribution supporting GTK 3.10 or later - for example, Ubuntu 14.04, Arch or Fedora 20
|
||||
|
||||
sudo apt-get install git gettext python-mako python-mutagen python-requests python-lxml
|
||||
for Debian & Debian-based distros such as Ubuntu & Mint:
|
||||
|
||||
for fedora and similar:
|
||||
sudo apt-get install git gettext python3-mako python3-lxml python3-gi-cairo python3-cairo gstreamer1.0-plugins-ugly gstreamer1.0-plugins-good gstreamer1.0-plugins-bad rhythmbox-plugins
|
||||
|
||||
yum install git gettext python-mako python-mutagen python-requests python-lxml
|
||||
for Fedora and similar:
|
||||
|
||||
Then install the plugin:
|
||||
sudo yum install git gettext python3-mako python3-lxml python3-cairo
|
||||
|
||||
NOTE: it is assumed that you have separately installed the patent encumbered codecs found in the good/bad & ugly packages
|
||||
To install the plugin:
|
||||
|
||||
<pre>
|
||||
rm -rf ~/.local/share/rhythmbox/plugins/coverart_browser
|
||||
git clone https://github.com/fossfreedom/coverart-browser.git
|
||||
git clone https://github.com/fossfreedom/coverart-browser.git -b master
|
||||
cd coverart-browser
|
||||
sh ./install.sh
|
||||
./install.sh
|
||||
</pre>
|
||||
|
||||
*For Ubuntu 12.04 & 12.10:*
|
||||
To uninstall the plugin:
|
||||
|
||||
This is now available in my rhythmbox PPA - installation instructions in this AskUbuntu Q&A:
|
||||
<pre>
|
||||
cd coverart-browser
|
||||
./install.sh --uninstall
|
||||
</pre>
|
||||
|
||||
Note 1 - the CoverArt Browser plugin also requires installing the following plugin:
|
||||
|
||||
- https://github.com/fossfreedom/coverart-search-providers
|
||||
|
||||
*For Ubuntu 14.04 and later:*
|
||||
|
||||
V2.0 is now available in my rhythmbox PPA - installation instructions in this AskUbuntu Q&A:
|
||||
|
||||
http://askubuntu.com/questions/147942/how-do-i-install-third-party-rhythmbox-plugins
|
||||
|
||||
**IMPORTANT NOTE**
|
||||
|
||||
For Ubuntu 12.04 users that have upgraded to Rhythmbox v2.98 using the webupd8 PPA, this version
|
||||
of rhythmbox crashes when used with python plugins such as coverart-browser and replaygain.
|
||||
|
||||
It is strongly recommended that you either upgrade to 12.10 where v2.98 works great, or
|
||||
downgrade to v2.96 or v2.97 as per:
|
||||
- http://askubuntu.com/questions/201093/how-do-i-downgrade-rhythmbox-v2-98
|
||||
|
||||
*installation for embedded coverart*
|
||||
|
||||
The plugin makes use of the package `python-mutagen`. For most distros, the default package is v1.20 which was released in 2010.
|
||||
|
||||
Since then, lots of bug fixes have been resolved. If you know that there is coverart embedded, but is not displayed
|
||||
in our plugin, then you should install the very latest package:
|
||||
|
||||
<pre>
|
||||
hg clone https://code.google.com/p/mutagen/
|
||||
</pre>
|
||||
|
||||
Then following the instructions in the README (slightly modified)
|
||||
|
||||
<pre>
|
||||
./setup.py build
|
||||
sudo su
|
||||
./setup.py install
|
||||
</pre>
|
||||
|
||||
Note - installing the package `rhythmbox-plugin-coverart-browser` will also install `rhythmbox-plugin-coverart-search`
|
||||
|
||||
**Please help out with translating**
|
||||
|
||||
We need you to help us translate the english text to your native language.
|
||||
|
||||
Don't worry - it is easier that you think.
|
||||
|
||||
Just visit:
|
||||
Don't worry - it is easier that you think. Just visit:
|
||||
|
||||
- https://translations.launchpad.net/coverartbrowser
|
||||
|
||||
Remember to set your preferred language and then just submit your translation.
|
||||
|
||||
Instructions are in the file TRANSLATE_README. Post a link to the file as a new issue, or
|
||||
if you are feeling generous - fork and push a pull-request. Thanks!
|
||||
|
||||
If they look scary - just email me (foss dot freedom at gmail dot com) and I'll send you the
|
||||
file that needs to be translated - it is less than 20 text strings so it should only take a
|
||||
few minutes.
|
||||
|
||||
When emailing - tell me your locale & language. You can find these by typing:
|
||||
|
||||
echo $LANG
|
||||
echo $LANGUAGE
|
||||
|
||||
-------
|
||||
|
||||
Authors:
|
||||
|
||||
The authors of this plugin are fossfreedom <foss.freedom@gmail.com>, Agustín Carrasco <asermax@gmail.com>
|
||||
|
||||
-------
|
||||
|
||||
Credits:
|
||||
|
||||
- thanks to Luqman Aden <laden@uwaterloo.ca> for the coverart-search plugin which our cover-search pane is based upon
|
||||
- thanks to Canonical for the Star widget which the ratings capabilities use
|
||||
- our Translators: Launchpad Translation team, jrbastien (fr_CA), asermax (es), mateuswetah (pt_BR), jrbastien & lannic (fr.po)
|
||||
- Button Icons - jrbastien for the new iconset
|
||||
- our Translators: Launchpad Translation team - individual credits for each locale is shown in the plugin preferences dialog
|
||||
- Button Icons - [jrbastien](https://github.com/jrbastien) for the five toolbar icon-sets
|
||||
- Flow view is based upon [Contentflow](http://jacksasylum.eu/ContentFlow)
|
||||
|
||||
Licenses:
|
||||
|
||||
This plugin code is released under the GPL3+ license.
|
||||
|
||||
Contentflow source is released under the MIT license
|
||||
|
||||
All translations are released under the BSD license
|
||||
|
||||
|
||||
@@ -5,19 +5,25 @@
|
||||
# Section 1: Developers Only
|
||||
#+++++++++++++++++++++++++++
|
||||
#for each ui file run the following to create translation .h files
|
||||
intltool-extract --type=gettext/glade ui/coverart_album_search_prefs.ui
|
||||
intltool-extract --type=gettext/glade ui/coverart_browser_prefs.ui
|
||||
intltool-extract --type=gettext/glade ui/coverart_browser.ui
|
||||
intltool-extract --type=gettext/glade ui/coverart_entryview.ui
|
||||
intltool-extract --type=gettext/glade ui/coverart_sidebar.ui
|
||||
intltool-extract --type=gettext/glade ui/coverart_topbar.ui
|
||||
intltool-extract --type=gettext/glade ui/toolbar_popup.ui
|
||||
intltool-extract --type=gettext/xml img/popups.xml.in
|
||||
intltool-extract --local --type=gettext/glade ui/coverart_artistview.ui
|
||||
intltool-extract --local --type=gettext/glade ui/coverart_artist_pop_rb3.ui
|
||||
intltool-extract --local --type=gettext/glade ui/coverart_browser_pop_rb3.ui
|
||||
intltool-extract --local --type=gettext/glade ui/coverart_browser_prefs.ui
|
||||
intltool-extract --local --type=gettext/glade ui/coverart_browser.ui
|
||||
intltool-extract --local --type=gettext/glade ui/coverart_entryview_compact_pop_rb3.ui
|
||||
intltool-extract --local --type=gettext/glade ui/coverart_entryview_full_pop_rb3.ui
|
||||
intltool-extract --local --type=gettext/glade ui/coverart_exportembed.ui
|
||||
intltool-extract --local --type=gettext/glade ui/coverart_leftsidebar.ui
|
||||
intltool-extract --local --type=gettext/glade ui/coverart_listwindow.ui
|
||||
intltool-extract --local --type=gettext/glade ui/coverart_play_pop_rb3.ui
|
||||
intltool-extract --local --type=gettext/glade ui/coverart_rightsidebar.ui
|
||||
intltool-extract --local --type=gettext/glade ui/coverart_topbar.ui
|
||||
intltool-extract --local --type=gettext/xml img/popups.xml.in
|
||||
|
||||
#create a new template file called po/package.pot by running below
|
||||
|
||||
#create a template file for the mako templates
|
||||
pybabel extract -F babel.cfg -o po/coverartbrowser.pot .
|
||||
pybabel extract -F babel.cfg -o po/coverartbrowser.pot -c "TRANSLATORS:" .
|
||||
|
||||
# po/files_to_be_translated are all the .h files generated by intltool
|
||||
xgettext -c -a -j -f po/files_to_be_translated -o po/coverartbrowser.pot
|
||||
@@ -25,16 +31,13 @@ xgettext -c -a -j -f po/files_to_be_translated -o po/coverartbrowser.pot
|
||||
# po/py_files_to_be_translated are all the python files to be translated
|
||||
xgettext -c -j -f po/py_files_to_be_translated -o po/coverartbrowser.pot
|
||||
|
||||
#now combine the pot files
|
||||
cd po
|
||||
|
||||
#now update existing po's with changes in the template file package.pot
|
||||
cd po
|
||||
./update_all_po.sh
|
||||
|
||||
#cleanup
|
||||
cd ..
|
||||
rm img/*.h
|
||||
rm ui/*.h
|
||||
rm -rf tmp
|
||||
|
||||
# Section 2: Translators
|
||||
#+++++++++++++++++++++++
|
||||
|
||||
@@ -1,346 +0,0 @@
|
||||
# -*- Mode: python; coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*-
|
||||
# Copyright (C) 2012 - fossfreedom
|
||||
# Copyright (C) 2012 - Agustin Carrasco
|
||||
## adapted from artsearch plugin - Copyright (C) 2012 Jonathan Matthew
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# The Rhythmbox authors hereby grant permission for non-GPL compatible
|
||||
# GStreamer plugins to be used and distributed together with GStreamer
|
||||
# and Rhythmbox. This permission is above and beyond the permissions granted
|
||||
# by the GPL license by which Rhythmbox is covered. If you modify this code
|
||||
# you may extend this exception to your version of the code, but you are not
|
||||
# obligated to do so. If you do not wish to do so, delete this exception
|
||||
# statement from your version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
from gi.repository import RB
|
||||
from gi.repository import GLib
|
||||
from gi.repository import Gdk
|
||||
from gi.repository import Gio
|
||||
|
||||
import os, time,re, urllib
|
||||
import threading
|
||||
import discogs_client as discogs
|
||||
|
||||
import json
|
||||
|
||||
import rb
|
||||
from gi.repository import RB
|
||||
|
||||
# coverartarchive URL
|
||||
COVERARTARCHIVE_RELEASE_URL = "http://coverartarchive.org/release/%s/"
|
||||
|
||||
ITEMS_PER_NOTIFICATION = 10
|
||||
IGNORED_SCHEMES = ('http', 'cdda', 'daap', 'mms')
|
||||
REPEAT_SEARCH_PERIOD = 86400 * 7
|
||||
|
||||
DISC_NUMBER_REGEXS = (
|
||||
"\(disc *[0-9]+\)",
|
||||
"\(cd *[0-9]+\)",
|
||||
"\[disc *[0-9]+\]",
|
||||
"\[cd *[0-9]+\]",
|
||||
" - disc *[0-9]+$",
|
||||
" - cd *[0-9]+$",
|
||||
" disc *[0-9]+$",
|
||||
" cd *[0-9]+$")
|
||||
|
||||
def file_root (f_name):
|
||||
return os.path.splitext (f_name)[0].lower ()
|
||||
|
||||
class CoverSearch(object):
|
||||
def __init__(self, store, key, last_time, searches):
|
||||
self.store = store
|
||||
self.key = key.copy()
|
||||
self.last_time = last_time
|
||||
self.searches = searches
|
||||
|
||||
def next_search(self):
|
||||
print "next search"
|
||||
if len(self.searches) == 0:
|
||||
print "no more searches"
|
||||
key = RB.ExtDBKey.create_storage("album", self.key.get_field("album"))
|
||||
key.add_field("artist", self.key.get_field("artist"))
|
||||
self.store.store(key, RB.ExtDBSourceType.NONE, None)
|
||||
print "end of next_search False"
|
||||
return False
|
||||
|
||||
search = self.searches.pop(0)
|
||||
print "calling search"
|
||||
search.search(self.key, self.last_time, self.store, self.search_done, None)
|
||||
print "end of next_search TRUE"
|
||||
return True
|
||||
|
||||
def search_done(self, args):
|
||||
self.next_search()
|
||||
|
||||
class CoverAlbumSearch:
|
||||
def __init__ (self):
|
||||
pass
|
||||
|
||||
def finished(self, results):
|
||||
parent = self.file.get_parent()
|
||||
|
||||
base = file_root (self.file.get_basename())
|
||||
for f_name in results:
|
||||
if file_root (f_name) == base:
|
||||
uri = parent.resolve_relative_path(f_name).get_parse_name()
|
||||
found = self.get_embedded_image(uri)
|
||||
if found:
|
||||
break
|
||||
|
||||
self.callback(self.callback_args)
|
||||
|
||||
def _enum_dir_cb(self, fileenum, result, results):
|
||||
try:
|
||||
files = fileenum.next_files_finish(result)
|
||||
if files is None or len(files) == 0:
|
||||
print "okay, done; got %d files" % len(results)
|
||||
self.finished(results)
|
||||
return
|
||||
|
||||
for f in files:
|
||||
ct = f.get_attribute_string("standard::content-type")
|
||||
# assume readable unless told otherwise
|
||||
readable = True
|
||||
if f.has_attribute("access::can-read"):
|
||||
readable = f.get_attribute_boolean("access::can-read")
|
||||
|
||||
if ct is not None and ct.startswith("audio/") and readable:
|
||||
print "_enum_dir_cb %s " % f.get_name()
|
||||
results.append(f.get_name())
|
||||
|
||||
fileenum.next_files_async(ITEMS_PER_NOTIFICATION, GLib.PRIORITY_DEFAULT, None, self._enum_dir_cb, results)
|
||||
except Exception, e:
|
||||
print "okay, probably done: %s" % e
|
||||
import sys
|
||||
sys.excepthook(*sys.exc_info())
|
||||
self.finished(results)
|
||||
|
||||
|
||||
def _enum_children_cb(self, parent, result, data):
|
||||
try:
|
||||
enumfiles = parent.enumerate_children_finish(result)
|
||||
enumfiles.next_files_async(ITEMS_PER_NOTIFICATION, GLib.PRIORITY_DEFAULT, None, self._enum_dir_cb, [])
|
||||
except Exception, e:
|
||||
print "okay, probably done: %s" % e
|
||||
import sys
|
||||
sys.excepthook(*sys.exc_info())
|
||||
self.callback(self.callback_args)
|
||||
|
||||
|
||||
def search (self, key, last_time, store, callback, args):
|
||||
# ignore last_time
|
||||
print "calling search"
|
||||
location = key.get_info("location")
|
||||
if location is None:
|
||||
print "not searching, we don't have a location"
|
||||
callback(args)
|
||||
return
|
||||
|
||||
self.file = Gio.file_new_for_uri(location)
|
||||
if self.file.get_uri_scheme() in IGNORED_SCHEMES:
|
||||
print 'not searching for local art for %s' % (self.file.get_uri())
|
||||
callback(args)
|
||||
return
|
||||
|
||||
self.album = key.get_field("album")
|
||||
self.artists = key.get_field_values("artist")
|
||||
self.store = store
|
||||
self.callback = callback
|
||||
self.callback_args = args
|
||||
|
||||
print 'searching for local art for %s' % (self.file.get_uri())
|
||||
parent = self.file.get_parent()
|
||||
enumfiles = parent.enumerate_children_async("standard::content-type,access::can-read,standard::name", 0, 0, None, self._enum_children_cb, None)
|
||||
|
||||
def get_embedded_image(self, search):
|
||||
print "get_embedded_image"
|
||||
import tempfile
|
||||
imagefilename = tempfile.NamedTemporaryFile(delete=False)
|
||||
|
||||
key = RB.ExtDBKey.create_storage("album", self.album)
|
||||
key.add_field("artist", self.artists[0])
|
||||
parent = self.file.get_parent()
|
||||
print parent
|
||||
print "possible mp4"
|
||||
try:
|
||||
from mutagen.mp4 import MP4
|
||||
mp = MP4(search)
|
||||
|
||||
if len(mp['covr']) >= 1:
|
||||
imagefilename.write(mp['covr'][0])
|
||||
uri = parent.resolve_relative_path(imagefilename.name).get_uri()
|
||||
imagefilename.close()
|
||||
self.store.store_uri(key, RB.ExtDBSourceType.USER, uri)
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
|
||||
print "possible flac"
|
||||
try:
|
||||
#flac
|
||||
from mutagen import File
|
||||
|
||||
music = File(search)
|
||||
imagefilename.write(music.pictures[0].data)
|
||||
imagefilename.close()
|
||||
uri = parent.resolve_relative_path(imagefilename.name).get_uri()
|
||||
self.store.store_uri(key, RB.ExtDBSourceType.USER, uri)
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
|
||||
print "possible ogg"
|
||||
try:
|
||||
from mutagen.oggvorbis import OggVorbis
|
||||
|
||||
o = OggVorbis(search)
|
||||
|
||||
try:
|
||||
pic=o['COVERART'][0]
|
||||
except:
|
||||
pic=o['METADATA_BLOCK_PICTURE'][0]
|
||||
|
||||
y=pic.decode('base64','strict')
|
||||
imagefilename.write(y)
|
||||
imagefilename.close()
|
||||
uri = parent.resolve_relative_path(imagefilename.name).get_uri()
|
||||
self.store.store_uri(key, RB.ExtDBSourceType.USER, uri)
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
|
||||
print "possible mp3"
|
||||
try:
|
||||
from mutagen.id3 import ID3
|
||||
i = ID3(search)
|
||||
|
||||
apic = i.getall('APIC')[0]
|
||||
imagefilename.write(apic.data)
|
||||
imagefilename.close()
|
||||
uri = parent.resolve_relative_path(imagefilename.name).get_uri()
|
||||
self.store.store_uri(key, RB.ExtDBSourceType.USER, uri)
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
|
||||
print "dont know"
|
||||
imagefilename.delete=True
|
||||
imagefilename.close()
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class DiscogsSearch (object):
|
||||
def __init__(self):
|
||||
discogs.user_agent = 'CoverartBrowserSearch/0.7alpha +https://github.com/fossfreedom/coverart-browser'
|
||||
|
||||
def search_url (self, artist, album):
|
||||
# Remove variants of Disc/CD [1-9] from album title before search
|
||||
orig_album = album
|
||||
for exp in DISC_NUMBER_REGEXS:
|
||||
p = re.compile (exp, re.IGNORECASE)
|
||||
album = p.sub ('', album)
|
||||
|
||||
album.strip()
|
||||
url = "%s/%s" % (artist,album)
|
||||
print "discogs url = %s" % url
|
||||
return url
|
||||
|
||||
def get_release_cb(self, store, searches, cbargs, callback):
|
||||
last_url = ""
|
||||
for search in searches:
|
||||
album = search[1]
|
||||
artist = search[0]
|
||||
url = self.search_url(album, artist)
|
||||
print "album %s artist %s url %s" % (album, artist, url)
|
||||
|
||||
if url == last_url:
|
||||
continue
|
||||
last_url = url
|
||||
try:
|
||||
s = discogs.Search(url)
|
||||
url = s.results()[0].data['images'][0]['uri150']
|
||||
current_key = RB.ExtDBKey.create_storage("album", album)
|
||||
current_key.add_field("artist", artist)
|
||||
store.store_uri(current_key, RB.ExtDBSourceType.SEARCH, url)
|
||||
print "got something"
|
||||
break
|
||||
except:
|
||||
pass
|
||||
|
||||
self.callback(cbargs)
|
||||
return False
|
||||
|
||||
def search(self, key, last_time, store, callback, args):
|
||||
if last_time > (time.time() - REPEAT_SEARCH_PERIOD):
|
||||
callback (args)
|
||||
return
|
||||
|
||||
album = key.get_field("album")
|
||||
artists = key.get_field_values("artist")
|
||||
artists = filter(lambda x: x not in (None, "", _("Unknown")), artists)
|
||||
if album in ("", _("Unknown")):
|
||||
album = None
|
||||
|
||||
if album == None or len(artists) == 0:
|
||||
callback (args)
|
||||
return
|
||||
|
||||
self.searches = []
|
||||
for a in artists:
|
||||
self.searches.append([a, album])
|
||||
|
||||
self.searches.append(["Various Artists", album])
|
||||
|
||||
self.callback = callback
|
||||
self.callback_args = args
|
||||
|
||||
threading.Thread( target=self.get_release_cb, args=(store, self.searches, args, callback)).start()
|
||||
|
||||
class CoverartArchiveSearch(object):
|
||||
|
||||
def get_release_cb (self, data, args):
|
||||
(key, store, callback, cbargs) = args
|
||||
if data is None:
|
||||
print "coverartarchive release request returned nothing"
|
||||
callback(*cbargs)
|
||||
return
|
||||
try:
|
||||
resp = json.loads(data)
|
||||
image_url = resp['images'][0]['image']
|
||||
print image_url
|
||||
|
||||
storekey = RB.ExtDBKey.create_storage('album', key.get_field('album'))
|
||||
storekey.add_field("artist", key.get_field("artist"))
|
||||
store.store_uri(storekey, RB.ExtDBSourceType.SEARCH, image_url)
|
||||
|
||||
callback(*cbargs)
|
||||
except Exception, e:
|
||||
print "exception parsing coverartarchive response: %s" % e
|
||||
callback(*cbargs)
|
||||
|
||||
def search(self, key, last_time, store, callback, *args):
|
||||
key = key.copy() # ugh
|
||||
album_id = key.get_info("musicbrainz-albumid")
|
||||
if album_id is None:
|
||||
print "no musicbrainz release ID for this track"
|
||||
callback(*args)
|
||||
return
|
||||
|
||||
url = COVERARTARCHIVE_RELEASE_URL % (album_id)
|
||||
print url
|
||||
loader = rb.Loader()
|
||||
loader.get_url(url, self.get_release_cb, (key, store, callback, args))
|
||||
@@ -1,36 +1,59 @@
|
||||
[Plugin]
|
||||
Loader=python
|
||||
Loader=python3
|
||||
Module=coverart_browser
|
||||
IAge=2
|
||||
Depends=rb
|
||||
Depends=rb;coverart_search_providers
|
||||
Name=CoverArt Browser
|
||||
Name[ast]=Navegador de portaes
|
||||
Name[az]=CoverArt gəzgini
|
||||
Name[bg]=CoverArt Browser
|
||||
Name[ca]=Navegador de Portades
|
||||
Name[cs]=Prohlížeč CoverArt
|
||||
Name[de]=CoverArt Browser
|
||||
Name[en_AU]=CoverArt Browser
|
||||
Name[en_GB]=CoverArt Browser
|
||||
Name[en_US]=CoverArt Browser
|
||||
Name[es]=Navegador de Portadas
|
||||
Name[es]=Navegador de carátulas
|
||||
Name[fi]=Kansikuvaselain
|
||||
Name[fr]=Navigateur de jaquettes
|
||||
Name[fr_CA]=Navigateur de pochettes
|
||||
Name[gl]=Navegador de cubertas
|
||||
Name[hr]=CoverArt Preglednik
|
||||
Name[it]=Gestore di CoverArt
|
||||
Name[ko]=앨범 표지 찾아보기
|
||||
Name[ms]=Pelayar CoverArt
|
||||
Name[pl]=Przeglądarka okładek
|
||||
Name[pt]=Arte das Capas
|
||||
Name[pt_BR]=Arte das Capas
|
||||
Name[ro]=Navigator CoverArt
|
||||
Name[ru]=Браузер обложек
|
||||
Name[zh_CN]=封面浏览器
|
||||
Description=Browse and play your albums through their covers
|
||||
Description[ast]=Navegar y reproducir los álbumes per aciu de les portaes
|
||||
Description[az]=Üz şəklinə görə albomlara bax və oxud
|
||||
Description[bg]=Търсете и слушайте албумите си посредством обложките им
|
||||
Description[ca]=Navega i reprodueix els àlbums mitjançant les seues portades
|
||||
Description[de]=Durchsuchen und geben Sie Ihre Alben über deren Cover wieder
|
||||
Description[en_AU]=Browse and play your albums through their covers
|
||||
Description[en_GB]=Browse and play your albums through their covers
|
||||
Description[es]=Navega y reproduce tu librería de álbums a través de sus portadas
|
||||
Description[en_US]=Browse and play your albums through their covers
|
||||
Description[es]=Navegue y reproduzca sus álbumes mediante sus carátulas
|
||||
Description[fi]=Selaa ja toista levyjäsi kansikuvien kautta
|
||||
Description[fr]=Parcourir les jaquettes de vos albums et les jouer
|
||||
Description[fr_CA]=Parcourir les pochettes de vos albums et les jouer
|
||||
Description[gl]=Buscar e reproducir os álbums a través das cubertas
|
||||
Description[hr]=Pregledavajte i slušajte albume preko omota
|
||||
Description[it]=Sfoglia e riproduce gli album per copertina
|
||||
Description[ko]=앨범을 표지로 찾아보고 연주합니다.
|
||||
Description[ms]=Layar dan mainkan album anda menerusi kulit album mereka
|
||||
Description[pl]=Przeglądaj i odtwarzaj Twoje albumy według ich okładek
|
||||
Description[pt]=Navegue e toque seus álbums através de suas capas
|
||||
Description[pt_BR]=Navegue e toque seus álbums através de suas capas
|
||||
Description[ro]=Parcurge și redă albumele prin intermediul coperților lor
|
||||
Description[ru]=Просматривайте и воспроизводите альбомы по обложкам
|
||||
Description[zh_CN]=浏览封面并播放专辑
|
||||
Authors=fossfreedom <foss.freedom@gmail.com>, Agustín Carrasco <asermax@gmail.com>
|
||||
Copyright=© 2012 fossfreedom, Agustín Carrasco © 2007 Alexandre Rosenfeld
|
||||
Website=http://github.com/fossfreedom
|
||||
|
||||
[RB]
|
||||
InitiallyEnabled=true
|
||||
Website=http://github.com/fossfreedom/coverart-browser
|
||||
Help=https://github.com/fossfreedom/coverart-browser/blob/release-2.0/README.md
|
||||
Version=2.0
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
[Plugin]
|
||||
Loader=python
|
||||
Loader=python3
|
||||
Module=coverart_browser
|
||||
IAge=2
|
||||
Depends=rb
|
||||
Depends=rb;coverart_search_providers
|
||||
_Name=CoverArt Browser
|
||||
_Description=Browse and play your albums through their covers
|
||||
Authors=fossfreedom <foss.freedom@gmail.com>, Agustín Carrasco <asermax@gmail.com>
|
||||
Copyright=© 2012 fossfreedom, Agustín Carrasco © 2007 Alexandre Rosenfeld
|
||||
Website=http://github.com/fossfreedom
|
||||
|
||||
[RB]
|
||||
InitiallyEnabled=true
|
||||
Website=http://github.com/fossfreedom/coverart-browser
|
||||
Help=https://github.com/fossfreedom/coverart-browser/blob/release-2.0/README.md
|
||||
Version=2.0
|
||||
|
||||
@@ -18,26 +18,31 @@
|
||||
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
# define plugin
|
||||
import rb
|
||||
import locale
|
||||
import gettext
|
||||
|
||||
|
||||
from gi.repository import GObject
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import RB
|
||||
from gi.repository import GdkPixbuf
|
||||
from gi.repository import Peas
|
||||
from gi.repository import Gio
|
||||
from gi.repository import GLib
|
||||
|
||||
from coverart_browser_prefs import Preferences
|
||||
import rb
|
||||
from coverart_browser_prefs import GSetting
|
||||
from coverart_browser_prefs import CoverLocale
|
||||
from coverart_browser_prefs import Preferences
|
||||
from coverart_browser_source import CoverArtBrowserSource
|
||||
from coverart_listview import ListView
|
||||
from coverart_queueview import QueueView
|
||||
from coverart_playsourceview import PlaySourceView
|
||||
from coverart_toolbar import TopToolbar
|
||||
from coverart_play_source import CoverArtPlaySource
|
||||
|
||||
|
||||
|
||||
class CoverArtBrowserEntryType(RB.RhythmDBEntryType):
|
||||
'''
|
||||
Entry type for our source.
|
||||
'''
|
||||
|
||||
def __init__(self):
|
||||
'''
|
||||
Initializes the entry type.
|
||||
@@ -52,13 +57,13 @@ class CoverArtBrowserPlugin(GObject.Object, Peas.Activatable):
|
||||
'''
|
||||
__gtype_name = 'CoverArtBrowserPlugin'
|
||||
object = GObject.property(type=GObject.Object)
|
||||
|
||||
|
||||
def __init__(self):
|
||||
'''
|
||||
Initialises the plugin object.
|
||||
'''
|
||||
GObject.Object.__init__(self)
|
||||
GObject.threads_init()
|
||||
self._externalmenu = None
|
||||
|
||||
def do_activate(self):
|
||||
'''
|
||||
@@ -67,60 +72,82 @@ class CoverArtBrowserPlugin(GObject.Object, Peas.Activatable):
|
||||
preferences.
|
||||
'''
|
||||
|
||||
#define .plugin text strings used for translation
|
||||
plugin = _('CoverArt Browser')
|
||||
desc = _('Browse and play your albums through their covers')
|
||||
|
||||
print "CoverArtBrowser DEBUG - do_activate"
|
||||
print("CoverArtBrowser DEBUG - do_activate")
|
||||
self.shell = self.object
|
||||
self.db = self.shell.props.db
|
||||
|
||||
try:
|
||||
entry_type = CoverArtBrowserEntryType()
|
||||
self.db.register_entry_type(entry_type)
|
||||
except NotImplementedError:
|
||||
entry_type = self.db.entry_register_type(
|
||||
'CoverArtBrowserEntryType')
|
||||
self.entry_type = CoverArtBrowserEntryType()
|
||||
self.db.register_entry_type(self.entry_type)
|
||||
|
||||
# we do some specific functionality when working with alternative toolbar
|
||||
# variables defined by the externalpluginmenu
|
||||
self.using_alternative_toolbar = False
|
||||
self.using_headerbar = False
|
||||
|
||||
cl = CoverLocale()
|
||||
cl.switch_locale(cl.Locale.LOCALE_DOMAIN)
|
||||
|
||||
entry_type.category = RB.RhythmDBEntryCategory.NORMAL
|
||||
|
||||
# load plugin icon
|
||||
theme = Gtk.IconTheme.get_default()
|
||||
rb.append_plugin_source_path(theme, '/icons')
|
||||
|
||||
what, width, height = Gtk.icon_size_lookup(Gtk.IconSize.LARGE_TOOLBAR)
|
||||
pxbf = GdkPixbuf.Pixbuf.new_from_file_at_size(
|
||||
rb.find_plugin_file(self, 'img/covermgr.png'), width, height)
|
||||
self.entry_type.category = RB.RhythmDBEntryCategory.NORMAL
|
||||
|
||||
group = RB.DisplayPageGroup.get_by_id('library')
|
||||
|
||||
theme = Gtk.IconTheme.get_default()
|
||||
theme.append_search_path(rb.find_plugin_file(self, 'img'))
|
||||
|
||||
iconfile = Gio.ThemedIcon(name = 'coverart-icon-symbolic')
|
||||
|
||||
self.source = CoverArtBrowserSource(shell=self.shell,
|
||||
name=_("CoverArt"), entry_type=entry_type,
|
||||
plugin=self, pixbuf=pxbf,
|
||||
# our plugin model shared between sources
|
||||
self.source_query_model = RB.RhythmDBQueryModel.new_empty(self.shell.props.db)
|
||||
|
||||
self.source = CoverArtBrowserSource(
|
||||
shell=self.shell,
|
||||
name=_("CoverArt"),
|
||||
entry_type=self.entry_type,
|
||||
plugin=self,
|
||||
icon=iconfile,
|
||||
query_model=self.shell.props.library_source.props.base_query_model)
|
||||
|
||||
self.shell.register_entry_type_for_source(self.source, entry_type)
|
||||
self.shell.register_entry_type_for_source(self.source, self.entry_type)
|
||||
self.source.props.visibility = False
|
||||
self.shell.append_display_page(self.source, group)
|
||||
|
||||
self.source.props.query_model.connect('complete', self.load_complete)
|
||||
self.playlist_source = GObject.new(
|
||||
CoverArtPlaySource,
|
||||
name=_("CoverArt Playlist"),
|
||||
shell=self.shell,
|
||||
plugin=self,
|
||||
entry_type=self.entry_type)
|
||||
|
||||
print "CoverArtBrowser DEBUG - end do_activate"
|
||||
self.shell.append_display_page(self.playlist_source, self.source)
|
||||
|
||||
self.shell.props.db.connect('load-complete', self.load_complete)
|
||||
# GLib.timeout_add_seconds(3, self.load_complete) # kludge - if plugin activated after RB has loaded then do stuff
|
||||
def delayed(*args):
|
||||
if self.shell.props.selected_page:
|
||||
self._externalmenu = ExternalPluginMenu(self)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
GLib.timeout_add(100, delayed)
|
||||
|
||||
cl.switch_locale(cl.Locale.RB)
|
||||
print("CoverArtBrowser DEBUG - end do_activate")
|
||||
|
||||
def do_deactivate(self):
|
||||
'''
|
||||
Called by Rhythmbox when the plugin is deactivated. It makes sure to
|
||||
free all the resources used by the plugin.
|
||||
'''
|
||||
print "CoverArtBrowser DEBUG - do_deactivate"
|
||||
print("CoverArtBrowser DEBUG - do_deactivate")
|
||||
self.source.delete_thyself()
|
||||
if self._externalmenu:
|
||||
self._externalmenu.cleanup(full_cleanup=True)
|
||||
del self.shell
|
||||
del self.db
|
||||
del self.source
|
||||
|
||||
print "CoverArtBrowser DEBUG - end do_deactivate"
|
||||
print("CoverArtBrowser DEBUG - end do_deactivate")
|
||||
|
||||
def load_complete(self, *args, **kwargs):
|
||||
'''
|
||||
@@ -128,9 +155,355 @@ class CoverArtBrowserPlugin(GObject.Object, Peas.Activatable):
|
||||
Used to automatically switch to the browser if the user
|
||||
has set in the preferences
|
||||
'''
|
||||
|
||||
gs = GSetting()
|
||||
setting = gs.get_setting(gs.Path.PLUGIN)
|
||||
|
||||
if setting[gs.PluginKey.AUTOSTART]:
|
||||
GObject.idle_add(self.shell.props.display_page_tree.select,
|
||||
self.source)
|
||||
self._externalmenu.autostart_source()
|
||||
|
||||
def _translation_helper(self):
|
||||
'''
|
||||
a method just to help out with translation strings
|
||||
it is not meant to be called by itself
|
||||
'''
|
||||
|
||||
# define .plugin text strings used for translation
|
||||
plugin = _('CoverArt Browser')
|
||||
desc = _('Browse and play your albums through their covers')
|
||||
|
||||
# . TRANSLATORS: This is the icon-grid view that the user sees
|
||||
tile = _('Tiles')
|
||||
|
||||
#. TRANSLATORS: This is the cover-flow view the user sees - they can swipe album covers from side-to-side
|
||||
artist = _('Flow')
|
||||
|
||||
#. TRANSLATORS: percentage size that the image will be expanded
|
||||
scale = _('Scale by %:')
|
||||
|
||||
# stop PyCharm removing the Preference import on optimisation
|
||||
pref = Preferences()
|
||||
|
||||
|
||||
class ExternalPluginMenu(GObject.Object):
|
||||
toolbar_pos = GObject.property(type=str, default=TopToolbar.name)
|
||||
|
||||
def __init__(self, plugin):
|
||||
super(ExternalPluginMenu, self).__init__()
|
||||
|
||||
self.plugin = plugin
|
||||
self.shell = plugin.shell
|
||||
self.source = plugin.source
|
||||
self.app_id = None
|
||||
self.locations = ['library-toolbar', 'queue-toolbar', 'playsource-toolbar']
|
||||
|
||||
from coverart_browser_source import Views
|
||||
|
||||
self._views = Views(self.shell)
|
||||
|
||||
self._use_standard_control = True
|
||||
self.plugin.using_alternative_toolbar = hasattr(self.shell, 'alternative_toolbar')
|
||||
if self.plugin.using_alternative_toolbar:
|
||||
from alttoolbar_type import AltToolbarHeaderBar
|
||||
self.plugin.using_headerbar = isinstance(self.shell.alternative_toolbar.toolbar_type, AltToolbarHeaderBar)
|
||||
|
||||
if self.plugin.using_headerbar:
|
||||
self._use_standard_control = False
|
||||
|
||||
# register with headerbar to complete the setup for coverart-browser
|
||||
print ("registering")
|
||||
self.shell.alternative_toolbar.toolbar_type.setup_completed_async(self._headerbar_toolbar_completed)
|
||||
|
||||
if self._use_standard_control:
|
||||
# ... otherwise just use the standard menubutton approach
|
||||
self.source.props.visibility = True # make the source visible
|
||||
gs = GSetting()
|
||||
setting = gs.get_setting(gs.Path.PLUGIN)
|
||||
setting.bind(gs.PluginKey.TOOLBAR_POS, self, 'toolbar_pos',
|
||||
Gio.SettingsBindFlags.GET)
|
||||
|
||||
self.connect('notify::toolbar-pos', self._on_notify_toolbar_pos)
|
||||
self.shell.props.display_page_tree.connect(
|
||||
"selected", self.on_page_change
|
||||
)
|
||||
|
||||
self._create_menu()
|
||||
|
||||
def autostart_source(self):
|
||||
self.source.props.visibility = True
|
||||
|
||||
if self._use_standard_control:
|
||||
GLib.timeout_add(1000, self.shell.props.display_page_tree.select,
|
||||
self.source)
|
||||
|
||||
else:
|
||||
# mimic user clicking category button and cover switch
|
||||
self.shell.alternative_toolbar.toolbar_type.library_browser_radiobutton.set_active(True)
|
||||
self.shell.alternative_toolbar.toolbar_type.stack.set_visible_child_name("coverview")
|
||||
|
||||
def _headerbar_toolbar_completed(self, *args):
|
||||
print ("headerbar_toolbar_completed")
|
||||
# if we are using the alternative_toolbar and headerbar then setup the switch
|
||||
# which will control access to the various views
|
||||
self._sh_hcc = self.shell.alternative_toolbar.toolbar_type.connect('song-category-clicked',
|
||||
self._headerbar_category_clicked)
|
||||
self._add_coverart_header_switch()
|
||||
|
||||
sources = { self.shell.props.queue_source,
|
||||
self.shell.props.library_source,
|
||||
self.source }
|
||||
|
||||
for source in sources:
|
||||
self.shell.alternative_toolbar.toolbar_type.add_always_visible_source(source)
|
||||
|
||||
def _on_notify_toolbar_pos(self, *args):
|
||||
# for standard menu control ... when moving the toolbar position reposition the menubutton
|
||||
if self.toolbar_pos == TopToolbar.name:
|
||||
self._create_menu()
|
||||
else:
|
||||
self.cleanup()
|
||||
|
||||
def cleanup(self, full_cleanup = False):
|
||||
# for standard menu control, cleanup where necessary
|
||||
if self.app_id:
|
||||
app = Gio.Application.get_default()
|
||||
for location in self.locations:
|
||||
app.remove_plugin_menu_item(location, self.app_id)
|
||||
self.app_id = None
|
||||
|
||||
if not self._use_standard_control and full_cleanup:
|
||||
self.shell.alternative_toolbar.toolbar_type.stack.disconnect(self._sh_stack_id)
|
||||
self.shell.alternative_toolbar.toolbar_type.stack.remove(self._box_coverview)
|
||||
self.shell.alternative_toolbar.toolbar_type.disconnect(self._sh_hcc)
|
||||
self.shell.alternative_toolbar.toolbar_type.headerbar.remove(self.stack_switcher)
|
||||
self.stack_switcher = None
|
||||
self._sh_stack_id = None
|
||||
self._sh_hcc = None
|
||||
|
||||
|
||||
def _create_menu(self):
|
||||
# for the standard menu control button add the button
|
||||
# to all supported view types
|
||||
app = Gio.Application.get_default()
|
||||
self.app_id = 'coverart-browser'
|
||||
|
||||
action_name = 'coverart-browser-views'
|
||||
self.action = Gio.SimpleAction.new_stateful(
|
||||
action_name, GLib.VariantType.new('s'),
|
||||
self._views.get_action_name(ListView.name)
|
||||
)
|
||||
self.action.connect("activate", self.view_change_cb)
|
||||
app.add_action(self.action)
|
||||
|
||||
menu_item = Gio.MenuItem()
|
||||
section = Gio.Menu()
|
||||
menu = Gio.Menu()
|
||||
toolbar_item = Gio.MenuItem()
|
||||
|
||||
for view_name in self._views.get_view_names():
|
||||
menu_item.set_label(self._views.get_menu_name(view_name))
|
||||
menu_item.set_action_and_target_value(
|
||||
'app.' + action_name, self._views.get_action_name(view_name)
|
||||
)
|
||||
section.append_item(menu_item)
|
||||
|
||||
menu.append_section(None, section)
|
||||
|
||||
cl = CoverLocale()
|
||||
cl.switch_locale(cl.Locale.LOCALE_DOMAIN)
|
||||
toolbar_item.set_label('…')
|
||||
cl.switch_locale(cl.Locale.RB)
|
||||
|
||||
toolbar_item.set_submenu(menu)
|
||||
for location in self.locations:
|
||||
app.add_plugin_menu_item(location, self.app_id, toolbar_item)
|
||||
|
||||
|
||||
def _add_coverart_header_switch(self):
|
||||
# define the header switch control + stack control for coverart
|
||||
self._box_coverview = Gtk.Box()
|
||||
image_name = 'view-cover-symbolic'
|
||||
stack = self.shell.alternative_toolbar.toolbar_type.stack
|
||||
stack.add_named(self._box_coverview, "coverview")
|
||||
stack.child_set_property(self._box_coverview, "icon-name", image_name)
|
||||
|
||||
self.stack_switcher = Gtk.StackSwitcher()
|
||||
self.stack_switcher.set_stack(stack)
|
||||
self.stack_switcher.show_all()
|
||||
self.stack_switcher.set_sensitive(False)
|
||||
|
||||
self.shell.alternative_toolbar.toolbar_type.headerbar.pack_start(self.stack_switcher)
|
||||
|
||||
# create a treeview and store for all views coverart supports
|
||||
self._store = Gtk.ListStore(str, str)
|
||||
for view_name in self._views.get_view_names():
|
||||
self._store.append([self._views.get_menu_name(view_name), view_name])
|
||||
|
||||
tree = Gtk.TreeView(self._store)
|
||||
renderer = Gtk.CellRendererText()
|
||||
column = Gtk.TreeViewColumn(_("CoverArt"), renderer, text=0)
|
||||
tree.append_column(column)
|
||||
tree.connect('button-press-event', self._tree_row_click)
|
||||
self.tree = tree
|
||||
|
||||
self._box_coverview.pack_start(tree, True, True, 0)
|
||||
|
||||
self._sh_stack_id = stack.connect('notify::visible-child-name', self._change_stack)
|
||||
stack.show_all()
|
||||
self.stack = stack
|
||||
|
||||
self._current_tree_view = None
|
||||
|
||||
def _change_stack(self, widget, value):
|
||||
print ("changed stack")
|
||||
child_name = self.stack.get_visible_child_name()
|
||||
print (child_name)
|
||||
if child_name == "listview":
|
||||
self.source.props.visibility = False
|
||||
# if we've toggled to listview then we are no longer in coverart so reset back to songview
|
||||
self._current_tree_view = None
|
||||
self._select_view(ListView.name)
|
||||
if self.shell.alternative_toolbar.toolbar_type.library_song_radiobutton.get_active():
|
||||
self.stack_switcher.set_sensitive(False)
|
||||
return
|
||||
self.source.props.visibility = True
|
||||
|
||||
# so we are in coverview so we need to reset the coverview to what was last selected when in this mode
|
||||
selection = self.tree.get_selection()
|
||||
liststore, list_iter = selection.get_selected()
|
||||
if not list_iter:
|
||||
# nothing was selected to set the view back to what was remembered
|
||||
self._current_tree_view = self._select_view(None)
|
||||
treeiter = liststore.get_iter_first()
|
||||
|
||||
while treeiter != None:
|
||||
if liststore[treeiter][1] == self._current_tree_view:
|
||||
print ("about to set treeview")
|
||||
print (treeiter)
|
||||
path = liststore.get_path(treeiter)
|
||||
print (path)
|
||||
#self.tree.row_activated(liststore.get_path(treeiter), 0)
|
||||
self.tree.set_cursor(path)
|
||||
break
|
||||
treeiter = liststore.iter_next(treeiter)
|
||||
else:
|
||||
# we have been here before so set the view correctly
|
||||
path = liststore.get_path(list_iter)
|
||||
self._current_tree_view = liststore[path][1]
|
||||
self._select_view(liststore[path][1])
|
||||
|
||||
def _headerbar_category_clicked(self, headerbar, song_category):
|
||||
|
||||
print ("clicked headerbar song-category buttons")
|
||||
if self.stack.get_visible_child_name() == 'coverview' and song_category:
|
||||
# if we've clicked song when in coverview then we disable the switcher
|
||||
# and set the view back to song
|
||||
|
||||
#self.stack.set_visible_child_name('listview')
|
||||
|
||||
#if self.shell.props.display_page_tree.select != self.shell.props.library_source:
|
||||
# self._select_view(ListView.name)
|
||||
|
||||
#self.stack_switcher.set_sensitive(not song_category)
|
||||
#self.stack_switcher.set_sensitive(False)
|
||||
self.source.props.visibility = True
|
||||
|
||||
self._select_view(ListView.name)
|
||||
|
||||
if self.stack.get_visible_child_name() == 'listview' and not song_category:
|
||||
# if we've clicked category when in listview then we enable the switcher
|
||||
self.stack_switcher.set_sensitive(True)
|
||||
self.source.props.visibility = False
|
||||
|
||||
|
||||
if self.stack.get_visible_child_name() == 'listview' and song_category:
|
||||
# if we've clicked song when in listview then we disable the switcher
|
||||
self.stack_switcher.set_sensitive(False)
|
||||
self.source.props.visibility = False
|
||||
|
||||
if self.stack.get_visible_child_name() == 'coverview' and not song_category:
|
||||
# if we've clicked category when in coverview then we move to the last coverart view
|
||||
# and ensure the switcher is still enabled
|
||||
self.source.props.visibility = True
|
||||
|
||||
self._select_view(None)
|
||||
self.stack_switcher.set_sensitive(True)
|
||||
|
||||
def _tree_row_click(self, widget, event):
|
||||
'''
|
||||
event called when clicking on a row in the header treeview
|
||||
'''
|
||||
print('_tree_row_click')
|
||||
|
||||
try:
|
||||
treepath, treecolumn, cellx, celly = widget.get_path_at_pos(event.x, event.y)
|
||||
except:
|
||||
return
|
||||
|
||||
print (self._store[treepath][1])
|
||||
self._current_tree_view = self._store[treepath][1]
|
||||
self._select_view(self._store[treepath][1])
|
||||
|
||||
|
||||
def on_page_change(self, display_page_tree, page):
|
||||
'''
|
||||
standard menubutton - Called when the display page changes. Grabs query models and sets the
|
||||
active view.
|
||||
'''
|
||||
print ("on_page_change")
|
||||
if page == self.shell.props.library_source:
|
||||
self.action.set_state(self._views.get_action_name(ListView.name))
|
||||
elif page == self.shell.props.queue_source:
|
||||
self.action.set_state(self._views.get_action_name(QueueView.name))
|
||||
elif page == self.plugin.playlist_source:
|
||||
self.action.set_state(self._views.get_action_name(PlaySourceView.name))
|
||||
|
||||
|
||||
def view_change_cb(self, action, current):
|
||||
'''
|
||||
standard menubutton - Called when the view state on a page is changed. Sets the new
|
||||
state.
|
||||
'''
|
||||
print ("view_change_cb")
|
||||
action.set_state(current)
|
||||
view_name = self._views.get_view_name_for_action(current)
|
||||
self._select_view(view_name)
|
||||
|
||||
def _select_view(self, view_name):
|
||||
'''
|
||||
with the view_name decide which view to be displayed
|
||||
or if view_name is None then use the last remembered view_name
|
||||
|
||||
return view_name
|
||||
'''
|
||||
|
||||
if not self.shell.props.display_page_tree:
|
||||
return
|
||||
|
||||
print ("_select_view")
|
||||
print (view_name)
|
||||
if view_name != ListView.name and \
|
||||
view_name != QueueView.name and \
|
||||
view_name != PlaySourceView.name:
|
||||
gs = GSetting()
|
||||
setting = gs.get_setting(gs.Path.PLUGIN)
|
||||
if view_name:
|
||||
setting[gs.PluginKey.VIEW_NAME] = view_name
|
||||
else:
|
||||
view_name = setting[gs.PluginKey.VIEW_NAME]
|
||||
player = self.shell.props.shell_player
|
||||
player.set_selected_source(self.source) #.playlist_source)
|
||||
|
||||
GLib.idle_add(self.shell.props.display_page_tree.select,
|
||||
self.source)
|
||||
elif view_name == ListView.name:
|
||||
GLib.idle_add(self.shell.props.display_page_tree.select,
|
||||
self.shell.props.library_source)
|
||||
elif view_name == QueueView.name:
|
||||
GLib.idle_add(self.shell.props.display_page_tree.select,
|
||||
self.shell.props.queue_source)
|
||||
elif view_name == PlaySourceView.name:
|
||||
GLib.idle_add(self.shell.props.display_page_tree.select,
|
||||
self.plugin.playlist_source)
|
||||
|
||||
return view_name
|
||||
|
||||
@@ -16,17 +16,34 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
import locale
|
||||
import gettext
|
||||
import os
|
||||
import shutil
|
||||
import webbrowser
|
||||
|
||||
from gi.repository import Gio
|
||||
from gi.repository import GObject
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import PeasGtk
|
||||
from gi.repository import Peas
|
||||
from gi.repository import RB
|
||||
from gi.repository import Gdk
|
||||
from gi.repository import GLib
|
||||
|
||||
import rb
|
||||
import locale
|
||||
import gettext
|
||||
from stars import ReactiveStar
|
||||
from stars import StarSize
|
||||
import coverart_rb3compat as rb3compat
|
||||
|
||||
|
||||
def webkit_support():
|
||||
'''
|
||||
function that returns True/False if webkit technology is supported
|
||||
'''
|
||||
gs = GSetting()
|
||||
settings = gs.get_setting(gs.Path.PLUGIN)
|
||||
return settings[gs.PluginKey.WEBKIT]
|
||||
|
||||
|
||||
class CoverLocale:
|
||||
@@ -41,7 +58,7 @@ class CoverLocale:
|
||||
# below public variables and methods that can be called for CoverLocale
|
||||
def __init__(self):
|
||||
'''
|
||||
Initializes the singleton interface, asigning all the constants
|
||||
Initializes the singleton interface, assigning all the constants
|
||||
used to access the plugin's settings.
|
||||
'''
|
||||
self.Locale = self._enum(
|
||||
@@ -94,7 +111,7 @@ class CoverLocale:
|
||||
|
||||
class GSetting:
|
||||
'''
|
||||
This class manages the differentes settings that the plugins haves to
|
||||
This class manages the different settings that the plugin has to
|
||||
access to read or write.
|
||||
'''
|
||||
# storage for the instance reference
|
||||
@@ -105,7 +122,7 @@ class GSetting:
|
||||
# below public variables and methods that can be called for GSetting
|
||||
def __init__(self):
|
||||
'''
|
||||
Initializes the singleton interface, asigning all the constants
|
||||
Initializes the singleton interface, assigning all the constants
|
||||
used to access the plugin's settings.
|
||||
'''
|
||||
self.Path = self._enum(
|
||||
@@ -116,8 +133,9 @@ class GSetting:
|
||||
|
||||
self.PluginKey = self._enum(
|
||||
CUSTOM_STATUSBAR='custom-statusbar',
|
||||
DISPLAY_BOTTOM='display-bottom',
|
||||
DISPLAY_TEXT='display-text',
|
||||
DISPLAY_TEXT_POS='display-text-pos',
|
||||
RANDOM='random-queue',
|
||||
DISPLAY_TEXT_LOADING='display-text-loading',
|
||||
DISPLAY_TEXT_ELLIPSIZE='display-text-ellipsize',
|
||||
DISPLAY_TEXT_ELLIPSIZE_LENGTH='display-text-ellipsize-length',
|
||||
@@ -128,12 +146,34 @@ class GSetting:
|
||||
PANED_POSITION='paned-position',
|
||||
SORT_BY='sort-by',
|
||||
SORT_ORDER='sort-order',
|
||||
SORT_BY_ARTIST='sort-by-artist',
|
||||
SORT_ORDER_ARTIST='sort-order-artist',
|
||||
RATING='rating-threshold',
|
||||
AUTOSTART='autostart',
|
||||
TOOLBAR_POS='toolbar-pos',
|
||||
EMBEDDED_SEARCH='embedded-search',
|
||||
DISCOGS_SEARCH='discogs-search',
|
||||
COVERARTARCHIVE_SEARCH='coverartarchive-search')
|
||||
BUTTON_RELIEF='button-relief',
|
||||
THEME='theme',
|
||||
NEW_GENRE_ICON='new-genre-icon',
|
||||
ICON_PADDING='icon-padding',
|
||||
ICON_SPACING='icon-spacing',
|
||||
ICON_AUTOMATIC='icon-automatic',
|
||||
VIEW_NAME='view-name',
|
||||
FLOW_APPEARANCE='flow-appearance',
|
||||
FLOW_HIDE_CAPTION='flow-hide-caption',
|
||||
FLOW_SCALE='flow-scale',
|
||||
FLOW_BACKGROUND_COLOUR='flow-background-colour',
|
||||
FLOW_AUTOMATIC='flow-automatic',
|
||||
FLOW_WIDTH='flow-width',
|
||||
FLOW_MAX='flow-max-albums',
|
||||
WEBKIT='webkit-support',
|
||||
ARTIST_PANED_POSITION='artist-paned-pos',
|
||||
USE_FAVOURITES='use-favourites',
|
||||
ARTIST_INFO_PANED_POSITION='artist-info-paned-pos',
|
||||
LAST_GENRE_FOLDER='last-genre-folder',
|
||||
ENTRY_VIEW_MODE='entry-view-mode',
|
||||
FOLLOWING='following',
|
||||
ACTIVATIONS='activations',
|
||||
TEXT_ALIGNMENT='text-alignment')
|
||||
|
||||
self.setting = {}
|
||||
|
||||
@@ -144,7 +184,7 @@ class GSetting:
|
||||
try:
|
||||
setting = self.setting[path]
|
||||
except:
|
||||
self.setting[path] = Gio.Settings(path)
|
||||
self.setting[path] = Gio.Settings.new(path)
|
||||
setting = self.setting[path]
|
||||
|
||||
return setting
|
||||
@@ -194,6 +234,9 @@ class Preferences(GObject.Object, PeasGtk.Configurable):
|
||||
__gtype_name__ = 'CoverArtBrowserPreferences'
|
||||
object = GObject.property(type=GObject.Object)
|
||||
|
||||
GENRE_POPUP = 1
|
||||
GENRE_LIST = 2
|
||||
|
||||
def __init__(self):
|
||||
'''
|
||||
Initialises the preferences, getting an instance of the settings saved
|
||||
@@ -203,63 +246,138 @@ class Preferences(GObject.Object, PeasGtk.Configurable):
|
||||
gs = GSetting()
|
||||
self.settings = gs.get_setting(gs.Path.PLUGIN)
|
||||
|
||||
self._first_run = True
|
||||
self._cover_size = 0
|
||||
self._cover_size_delay = 0
|
||||
|
||||
def do_create_configure_widget(self):
|
||||
'''
|
||||
Creates the plugin's preferences dialog
|
||||
'''
|
||||
return self._create_display_contents(self)
|
||||
|
||||
def display_preferences_dialog(self, plugin):
|
||||
print("DEBUG - display_preferences_dialog")
|
||||
if self._first_run:
|
||||
self._first_run = False
|
||||
|
||||
cl = CoverLocale()
|
||||
cl.switch_locale(cl.Locale.LOCALE_DOMAIN)
|
||||
|
||||
self._dialog = Gtk.Dialog(modal=True, destroy_with_parent=True)
|
||||
self._dialog.add_button(Gtk.STOCK_OK, Gtk.ResponseType.OK)
|
||||
self._dialog.set_title(_('Browser Preferences'))
|
||||
content_area = self._dialog.get_content_area()
|
||||
content_area.pack_start(self._create_display_contents(plugin), True, True, 0)
|
||||
|
||||
helpbutton = self._dialog.add_button(Gtk.STOCK_HELP, Gtk.ResponseType.HELP)
|
||||
helpbutton.connect('clicked', self._display_help)
|
||||
|
||||
self._dialog.show_all()
|
||||
|
||||
print("shown")
|
||||
|
||||
while True:
|
||||
response = self._dialog.run()
|
||||
|
||||
print("and run")
|
||||
|
||||
if response != Gtk.ResponseType.HELP:
|
||||
break
|
||||
|
||||
self._dialog.hide()
|
||||
|
||||
print("DEBUG - display_preferences_dialog end")
|
||||
|
||||
def _display_help(self, *args):
|
||||
peas = Peas.Engine.get_default()
|
||||
uri = peas.get_plugin_info('coverart_browser').get_help_uri()
|
||||
|
||||
webbrowser.open(uri)
|
||||
|
||||
def _create_display_contents(self, plugin):
|
||||
print("DEBUG - create_display_contents")
|
||||
# create the ui
|
||||
self._first_run = True
|
||||
cl = CoverLocale()
|
||||
cl.switch_locale(cl.Locale.LOCALE_DOMAIN)
|
||||
builder = Gtk.Builder()
|
||||
builder.add_from_file(rb.find_plugin_file(self,
|
||||
'ui/coverart_browser_prefs.ui'))
|
||||
builder.set_translation_domain(cl.Locale.LOCALE_DOMAIN)
|
||||
builder.add_from_file(rb.find_plugin_file(plugin,
|
||||
'ui/coverart_browser_prefs.ui'))
|
||||
self.launchpad_button = builder.get_object('show_launchpad')
|
||||
self.launchpad_label = builder.get_object('launchpad_label')
|
||||
|
||||
builder.connect_signals(self)
|
||||
|
||||
# . TRANSLATORS: Do not translate this string.
|
||||
translators = _('translator-credits')
|
||||
|
||||
if translators != "translator-credits":
|
||||
self.launchpad_label.set_text(translators)
|
||||
else:
|
||||
self.launchpad_button.set_visible(False)
|
||||
|
||||
gs = GSetting()
|
||||
# bind the toggles to the settings
|
||||
toggle_statusbar = builder.get_object('custom_statusbar_checkbox')
|
||||
self.settings.bind(gs.PluginKey.CUSTOM_STATUSBAR,
|
||||
toggle_statusbar, 'active', Gio.SettingsBindFlags.DEFAULT)
|
||||
|
||||
toggle_bottom = builder.get_object('display_bottom_checkbox')
|
||||
self.settings.bind(gs.PluginKey.DISPLAY_BOTTOM, toggle_bottom,
|
||||
'active', Gio.SettingsBindFlags.DEFAULT)
|
||||
toggle_statusbar, 'active', Gio.SettingsBindFlags.DEFAULT)
|
||||
|
||||
toggle_text = builder.get_object('display_text_checkbox')
|
||||
self.settings.bind(gs.PluginKey.DISPLAY_TEXT, toggle_text, 'active',
|
||||
Gio.SettingsBindFlags.DEFAULT)
|
||||
Gio.SettingsBindFlags.DEFAULT)
|
||||
|
||||
box_text = builder.get_object('display_text_box')
|
||||
self.settings.bind(gs.PluginKey.DISPLAY_TEXT, box_text, 'sensitive',
|
||||
Gio.SettingsBindFlags.GET)
|
||||
Gio.SettingsBindFlags.GET)
|
||||
|
||||
self.display_text_pos = self.settings[gs.PluginKey.DISPLAY_TEXT_POS]
|
||||
self.display_text_under_radiobutton = builder.get_object('display_text_under_radiobutton')
|
||||
self.display_text_within_radiobutton = builder.get_object('display_text_within_radiobutton')
|
||||
|
||||
if self.display_text_pos:
|
||||
self.display_text_under_radiobutton.set_active(True)
|
||||
else:
|
||||
self.display_text_within_radiobutton.set_active(True)
|
||||
|
||||
random_scale = builder.get_object('random_adjustment')
|
||||
self.settings.bind(gs.PluginKey.RANDOM, random_scale, 'value',
|
||||
Gio.SettingsBindFlags.DEFAULT)
|
||||
|
||||
toggle_text_ellipsize = builder.get_object(
|
||||
'display_text_ellipsize_checkbox')
|
||||
self.settings.bind(gs.PluginKey.DISPLAY_TEXT_ELLIPSIZE,
|
||||
toggle_text_ellipsize, 'active', Gio.SettingsBindFlags.DEFAULT)
|
||||
toggle_text_ellipsize, 'active', Gio.SettingsBindFlags.DEFAULT)
|
||||
|
||||
box_text_ellipsize_length = builder.get_object(
|
||||
'display_text_ellipsize_length_box')
|
||||
self.settings.bind(gs.PluginKey.DISPLAY_TEXT_ELLIPSIZE,
|
||||
box_text_ellipsize_length, 'sensitive', Gio.SettingsBindFlags.GET)
|
||||
box_text_ellipsize_length, 'sensitive', Gio.SettingsBindFlags.GET)
|
||||
|
||||
spinner_text_ellipsize_length = builder.get_object(
|
||||
'display_text_ellipsize_length_spin')
|
||||
self.settings.bind(gs.PluginKey.DISPLAY_TEXT_ELLIPSIZE_LENGTH,
|
||||
spinner_text_ellipsize_length, 'value',
|
||||
Gio.SettingsBindFlags.DEFAULT)
|
||||
spinner_text_ellipsize_length, 'value',
|
||||
Gio.SettingsBindFlags.DEFAULT)
|
||||
|
||||
spinner_font_size = builder.get_object(
|
||||
'display_font_spin')
|
||||
self.settings.bind(gs.PluginKey.DISPLAY_FONT_SIZE,
|
||||
spinner_font_size, 'value',
|
||||
Gio.SettingsBindFlags.DEFAULT)
|
||||
spinner_font_size, 'value',
|
||||
Gio.SettingsBindFlags.DEFAULT)
|
||||
|
||||
cover_size_scale = builder.get_object('cover_size_adjustment')
|
||||
self.settings.bind(gs.PluginKey.COVER_SIZE, cover_size_scale, 'value',
|
||||
Gio.SettingsBindFlags.DEFAULT)
|
||||
|
||||
#self.settings.bind(gs.PluginKey.COVER_SIZE, cover_size_scale, 'value',
|
||||
# Gio.SettingsBindFlags.DEFAULT)
|
||||
self._cover_size = self.settings[gs.PluginKey.COVER_SIZE]
|
||||
cover_size_scale.set_value(self._cover_size)
|
||||
cover_size_scale.connect('value-changed', self.on_cover_size_scale_changed)
|
||||
|
||||
add_shadow = builder.get_object('add_shadow_checkbox')
|
||||
self.settings.bind(gs.PluginKey.ADD_SHADOW, add_shadow, 'active',
|
||||
Gio.SettingsBindFlags.DEFAULT)
|
||||
Gio.SettingsBindFlags.DEFAULT)
|
||||
|
||||
rated_box = builder.get_object('rated_box')
|
||||
self.stars = ReactiveStar(size=StarSize.BIG)
|
||||
@@ -274,34 +392,387 @@ class Preferences(GObject.Object, PeasGtk.Configurable):
|
||||
|
||||
autostart = builder.get_object('autostart_checkbox')
|
||||
self.settings.bind(gs.PluginKey.AUTOSTART,
|
||||
autostart, 'active', Gio.SettingsBindFlags.DEFAULT)
|
||||
|
||||
embedded_search = builder.get_object('embedded_checkbox')
|
||||
self.settings.bind(gs.PluginKey.EMBEDDED_SEARCH,
|
||||
embedded_search, 'active', Gio.SettingsBindFlags.DEFAULT)
|
||||
|
||||
discogs_search = builder.get_object('discogs_checkbox')
|
||||
self.settings.bind(gs.PluginKey.DISCOGS_SEARCH,
|
||||
discogs_search, 'active', Gio.SettingsBindFlags.DEFAULT)
|
||||
autostart, 'active', Gio.SettingsBindFlags.DEFAULT)
|
||||
|
||||
toolbar_pos_combo = builder.get_object('show_in_combobox')
|
||||
renderer = Gtk.CellRendererText()
|
||||
toolbar_pos_combo.pack_start(renderer, True)
|
||||
toolbar_pos_combo.add_attribute(renderer, 'text', 1)
|
||||
self.settings.bind(gs.PluginKey.TOOLBAR_POS, toolbar_pos_combo,
|
||||
'active-id', Gio.SettingsBindFlags.DEFAULT)
|
||||
'active-id', Gio.SettingsBindFlags.DEFAULT)
|
||||
|
||||
light_source_combo = builder.get_object('light_source_combobox')
|
||||
renderer = Gtk.CellRendererText()
|
||||
light_source_combo.pack_start(renderer, True)
|
||||
light_source_combo.add_attribute(renderer, 'text', 1)
|
||||
self.settings.bind(gs.PluginKey.SHADOW_IMAGE, light_source_combo,
|
||||
'active-id', Gio.SettingsBindFlags.DEFAULT)
|
||||
'active-id', Gio.SettingsBindFlags.DEFAULT)
|
||||
|
||||
combo_liststore = builder.get_object('combo_liststore')
|
||||
|
||||
from coverart_utils import Theme
|
||||
|
||||
for theme in Theme(self).themes:
|
||||
combo_liststore.append([theme, theme])
|
||||
|
||||
theme_combo = builder.get_object('theme_combobox')
|
||||
renderer = Gtk.CellRendererText()
|
||||
theme_combo.pack_start(renderer, True)
|
||||
theme_combo.add_attribute(renderer, 'text', 1)
|
||||
self.settings.bind(gs.PluginKey.THEME, theme_combo,
|
||||
'active-id', Gio.SettingsBindFlags.DEFAULT)
|
||||
|
||||
button_relief = builder.get_object('button_relief_checkbox')
|
||||
self.settings.bind(gs.PluginKey.BUTTON_RELIEF, button_relief, 'active',
|
||||
Gio.SettingsBindFlags.DEFAULT)
|
||||
|
||||
# create user data files
|
||||
cachedir = RB.user_cache_dir() + "/coverart_browser/usericons"
|
||||
if not os.path.exists(cachedir):
|
||||
os.makedirs(cachedir)
|
||||
|
||||
popup = cachedir + "/popups.xml"
|
||||
|
||||
temp = RB.find_user_data_file('plugins/coverart_browser/img/usericons/popups.xml')
|
||||
|
||||
# lets see if there is a legacy file - if necessary copy it to the cache dir
|
||||
if os.path.isfile(temp) and not os.path.isfile(popup):
|
||||
shutil.copyfile(temp, popup)
|
||||
|
||||
if not os.path.isfile(popup):
|
||||
template = rb.find_plugin_file(plugin, 'template/popups.xml')
|
||||
folder = os.path.split(popup)[0]
|
||||
if not os.path.exists(folder):
|
||||
os.makedirs(folder)
|
||||
shutil.copyfile(template, popup)
|
||||
|
||||
# now prepare the genre tab
|
||||
from coverart_utils import GenreConfiguredSpriteSheet
|
||||
from coverart_utils import get_stock_size
|
||||
|
||||
self._sheet = GenreConfiguredSpriteSheet(plugin, "genre", get_stock_size())
|
||||
|
||||
self.alt_liststore = builder.get_object('alt_liststore')
|
||||
self.alt_user_liststore = builder.get_object('alt_user_liststore')
|
||||
self._iters = {}
|
||||
for key in list(self._sheet.keys()):
|
||||
store_iter = self.alt_liststore.append([key, self._sheet[key]])
|
||||
self._iters[(key, self.GENRE_POPUP)] = store_iter
|
||||
|
||||
for key, value in self._sheet.genre_alternate.items():
|
||||
if key.genre_type == GenreConfiguredSpriteSheet.GENRE_USER:
|
||||
store_iter = self.alt_user_liststore.append([key.name,
|
||||
self._sheet[self._sheet.genre_alternate[key]],
|
||||
self._sheet.genre_alternate[key]])
|
||||
self._iters[(key.name, self.GENRE_LIST)] = store_iter
|
||||
|
||||
self.amend_mode = False
|
||||
self.blank_iter = None
|
||||
self.genre_combobox = builder.get_object('genre_combobox')
|
||||
self.genre_entry = builder.get_object('genre_entry')
|
||||
self.genre_view = builder.get_object('genre_view')
|
||||
self.save_button = builder.get_object('save_button')
|
||||
self.filechooserdialog = builder.get_object('filechooserdialog')
|
||||
last_genre_folder = self.settings[gs.PluginKey.LAST_GENRE_FOLDER]
|
||||
if last_genre_folder != "":
|
||||
self.filechooserdialog.set_current_folder(last_genre_folder)
|
||||
|
||||
padding_scale = builder.get_object('padding_adjustment')
|
||||
self.settings.bind(gs.PluginKey.ICON_PADDING, padding_scale, 'value',
|
||||
Gio.SettingsBindFlags.DEFAULT)
|
||||
|
||||
spacing_scale = builder.get_object('spacing_adjustment')
|
||||
self.settings.bind(gs.PluginKey.ICON_SPACING, spacing_scale, 'value',
|
||||
Gio.SettingsBindFlags.DEFAULT)
|
||||
|
||||
icon_automatic = builder.get_object('icon_automatic_checkbox')
|
||||
self.settings.bind(gs.PluginKey.ICON_AUTOMATIC,
|
||||
icon_automatic, 'active', Gio.SettingsBindFlags.DEFAULT)
|
||||
|
||||
#flow tab
|
||||
flow_combo = builder.get_object('flow_combobox')
|
||||
renderer = Gtk.CellRendererText()
|
||||
flow_combo.pack_start(renderer, True)
|
||||
flow_combo.add_attribute(renderer, 'text', 1)
|
||||
self.settings.bind(gs.PluginKey.FLOW_APPEARANCE, flow_combo,
|
||||
'active-id', Gio.SettingsBindFlags.DEFAULT)
|
||||
|
||||
flow_hide = builder.get_object('hide_caption_checkbox')
|
||||
self.settings.bind(gs.PluginKey.FLOW_HIDE_CAPTION,
|
||||
flow_hide, 'active', Gio.SettingsBindFlags.DEFAULT)
|
||||
|
||||
flow_scale = builder.get_object('cover_scale_adjustment')
|
||||
self.settings.bind(gs.PluginKey.FLOW_SCALE, flow_scale, 'value',
|
||||
Gio.SettingsBindFlags.DEFAULT)
|
||||
|
||||
flow_width = builder.get_object('cover_width_adjustment')
|
||||
self.settings.bind(gs.PluginKey.FLOW_WIDTH, flow_width, 'value',
|
||||
Gio.SettingsBindFlags.DEFAULT)
|
||||
|
||||
flow_max = builder.get_object('flow_max_adjustment')
|
||||
self.settings.bind(gs.PluginKey.FLOW_MAX, flow_max, 'value',
|
||||
Gio.SettingsBindFlags.DEFAULT)
|
||||
|
||||
flow_automatic = builder.get_object('automatic_checkbox')
|
||||
self.settings.bind(gs.PluginKey.FLOW_AUTOMATIC,
|
||||
flow_automatic, 'active', Gio.SettingsBindFlags.DEFAULT)
|
||||
|
||||
self.background_colour = self.settings[gs.PluginKey.FLOW_BACKGROUND_COLOUR]
|
||||
self.white_radiobutton = builder.get_object('white_radiobutton')
|
||||
self.black_radiobutton = builder.get_object('black_radiobutton')
|
||||
|
||||
if self.background_colour == 'W':
|
||||
self.white_radiobutton.set_active(True)
|
||||
else:
|
||||
self.black_radiobutton.set_active(True)
|
||||
|
||||
self.text_alignment = self.settings[gs.PluginKey.TEXT_ALIGNMENT]
|
||||
self.text_alignment_left_radiobutton = builder.get_object('left_alignment_radiobutton')
|
||||
self.text_alignment_centre_radiobutton = builder.get_object('centre_alignment_radiobutton')
|
||||
self.text_alignment_right_radiobutton = builder.get_object('right_alignment_radiobutton')
|
||||
|
||||
if self.text_alignment == 0:
|
||||
self.text_alignment_left_radiobutton.set_active(True)
|
||||
elif self.text_alignment == 1:
|
||||
self.text_alignment_centre_radiobutton.set_active(True)
|
||||
else:
|
||||
self.text_alignment_right_radiobutton.set_active(True)
|
||||
|
||||
# return the dialog
|
||||
self._first_run = False
|
||||
print("end create dialog contents")
|
||||
return builder.get_object('main_notebook')
|
||||
|
||||
def on_cover_size_scale_changed(self, scale):
|
||||
self._cover_size = scale.get_value()
|
||||
|
||||
def delay(*args):
|
||||
print('delay')
|
||||
print(self._cover_size_delay)
|
||||
self._cover_size_delay = self._cover_size_delay + 1
|
||||
|
||||
if self._cover_size_delay >= 8:
|
||||
gs = GSetting()
|
||||
self.settings[gs.PluginKey.COVER_SIZE] = self._cover_size
|
||||
self._cover_size_delay = 0
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
if self._cover_size_delay == 0:
|
||||
Gdk.threads_add_timeout(GLib.PRIORITY_DEFAULT_IDLE, 100, delay, None)
|
||||
else:
|
||||
self._cover_size_delay = 1
|
||||
|
||||
def on_flow_combobox_changed(self, combobox):
|
||||
current_val = combobox.get_model()[combobox.get_active()][0]
|
||||
gs = GSetting()
|
||||
if self.settings[gs.PluginKey.FLOW_APPEARANCE] != current_val:
|
||||
if current_val == 'flow-vert':
|
||||
default_size = 150
|
||||
else:
|
||||
default_size = 600
|
||||
|
||||
self.settings[gs.PluginKey.FLOW_WIDTH] = default_size
|
||||
|
||||
if current_val == 'carousel':
|
||||
self.settings[gs.PluginKey.FLOW_HIDE_CAPTION] = True
|
||||
|
||||
def on_background_radio_toggled(self, button):
|
||||
if button.get_active():
|
||||
gs = GSetting()
|
||||
if button == self.white_radiobutton:
|
||||
self.settings[gs.PluginKey.FLOW_BACKGROUND_COLOUR] = 'W'
|
||||
else:
|
||||
self.settings[gs.PluginKey.FLOW_BACKGROUND_COLOUR] = 'B'
|
||||
|
||||
def on_display_text_pos_radio_toggled(self, button):
|
||||
if self._first_run:
|
||||
return
|
||||
|
||||
if button.get_active():
|
||||
gs = GSetting()
|
||||
if button == self.display_text_under_radiobutton:
|
||||
self.settings[gs.PluginKey.DISPLAY_TEXT_POS] = True
|
||||
else:
|
||||
self.settings[gs.PluginKey.DISPLAY_TEXT_POS] = False
|
||||
self.settings[gs.PluginKey.ADD_SHADOW] = False
|
||||
|
||||
def on_text_alignment_radiobutton_toggled(self, button):
|
||||
if self._first_run:
|
||||
return
|
||||
|
||||
if button.get_active():
|
||||
gs = GSetting()
|
||||
if button == self.text_alignment_left_radiobutton:
|
||||
self.settings[gs.PluginKey.TEXT_ALIGNMENT] = 0
|
||||
elif button == self.text_alignment_centre_radiobutton:
|
||||
self.settings[gs.PluginKey.TEXT_ALIGNMENT] = 1
|
||||
else:
|
||||
self.settings[gs.PluginKey.TEXT_ALIGNMENT] = 2
|
||||
|
||||
def on_add_shadow_checkbox_toggled(self, button):
|
||||
if button.get_active():
|
||||
# gs = GSetting()
|
||||
#self.settings[gs.PluginKey.DISPLAY_TEXT_POS] = True
|
||||
self.display_text_under_radiobutton.set_active(True)
|
||||
|
||||
def rating_changed_callback(self, stars):
|
||||
print "rating_changed_callback"
|
||||
print("rating_changed_callback")
|
||||
gs = GSetting()
|
||||
self.settings[gs.PluginKey.RATING] = self.stars.get_rating()
|
||||
|
||||
def on_save_button_clicked(self, button):
|
||||
'''
|
||||
action when genre edit area is saved
|
||||
'''
|
||||
entry_value = self.genre_entry.get_text()
|
||||
treeiter = self.genre_combobox.get_active_iter()
|
||||
icon_value = self.alt_liststore[treeiter][0]
|
||||
# model 0 is the icon name, model 1 is the pixbuf
|
||||
|
||||
if self.amend_mode:
|
||||
key = self._sheet.amend_genre_info(self.current_genre,
|
||||
entry_value, icon_value)
|
||||
|
||||
self.alt_user_liststore[self._iters[(self.current_genre,
|
||||
self.GENRE_LIST)]][1] = self._sheet[self._sheet.genre_alternate[key]]
|
||||
self.alt_user_liststore[self._iters[(self.current_genre,
|
||||
self.GENRE_LIST)]][0] = key.name
|
||||
store_iter = self._iters[(self.current_genre, self.GENRE_LIST)]
|
||||
del self._iters[(self.current_genre, self.GENRE_LIST)]
|
||||
self._iters[(key.name, self.GENRE_LIST)] = store_iter
|
||||
|
||||
else:
|
||||
self.amend_mode = True
|
||||
key = self._sheet.amend_genre_info('',
|
||||
entry_value, icon_value)
|
||||
self.current_genre = key.name
|
||||
|
||||
store_iter = self.alt_user_liststore.append([key.name,
|
||||
self._sheet[self._sheet.genre_alternate[key]],
|
||||
self._sheet.genre_alternate[key]])
|
||||
self._iters[(key.name, self.GENRE_LIST)] = store_iter
|
||||
selection = self.genre_view.get_selection()
|
||||
selection.select_iter(store_iter)
|
||||
|
||||
self.save_button.set_sensitive(False)
|
||||
self._toggle_new_genre_state()
|
||||
|
||||
|
||||
def on_genre_filechooserbutton_file_set(self, filechooser):
|
||||
'''
|
||||
action when genre new icon button is pressed
|
||||
'''
|
||||
key = self._sheet.add_genre_icon(self.filechooserdialog.get_filename())
|
||||
store_iter = self.alt_liststore.append([key.name, self._sheet[key.name]])
|
||||
self._iters[(key.name, self.GENRE_POPUP)] = store_iter
|
||||
|
||||
gs = GSetting()
|
||||
last_genre_folder = self.filechooserdialog.get_current_folder()
|
||||
|
||||
print(last_genre_folder)
|
||||
print(self.filechooserdialog.get_filename())
|
||||
if last_genre_folder:
|
||||
self.settings[gs.PluginKey.LAST_GENRE_FOLDER] = last_genre_folder
|
||||
|
||||
def on_genre_view_selection_changed(self, view):
|
||||
'''
|
||||
action when user selects a row in the list of genres
|
||||
'''
|
||||
model, genre_iter = view.get_selected()
|
||||
if genre_iter:
|
||||
self.genre_entry.set_text(model[genre_iter][0])
|
||||
index = model[genre_iter][2]
|
||||
if index != '':
|
||||
self.genre_combobox.set_active_iter(self._iters[(index, self.GENRE_POPUP)])
|
||||
self.amend_mode = True
|
||||
self.current_genre = rb3compat.unicodestr(model[genre_iter][0], 'utf-8')
|
||||
else:
|
||||
self.genre_entry.set_text('')
|
||||
self.genre_combobox.set_active_iter(None)
|
||||
self.amend_mode = False
|
||||
|
||||
if self.blank_iter and self.amend_mode:
|
||||
try:
|
||||
index = model[self.blank_iter][0]
|
||||
if index == '':
|
||||
model.remove(self.blank_iter)
|
||||
self.blank_iter = None
|
||||
except:
|
||||
self.blank_iter = None
|
||||
|
||||
def on_add_button_clicked(self, button):
|
||||
'''
|
||||
action when a new genre is added to the table
|
||||
'''
|
||||
self.genre_entry.set_text('')
|
||||
self.genre_combobox.set_active(-1)
|
||||
self.amend_mode = False
|
||||
self.blank_iter = self.alt_user_liststore.append(['', None, ''])
|
||||
selection = self.genre_view.get_selection()
|
||||
selection.select_iter(self.blank_iter)
|
||||
|
||||
|
||||
def on_delete_button_clicked(self, button):
|
||||
'''
|
||||
action when a genre is to be deleted
|
||||
'''
|
||||
selection = self.genre_view.get_selection()
|
||||
|
||||
model, genre_iter = selection.get_selected()
|
||||
if genre_iter:
|
||||
index = rb3compat.unicodestr(model[genre_iter][0], 'utf-8')
|
||||
model.remove(genre_iter)
|
||||
|
||||
if index:
|
||||
del self._iters[(index, self.GENRE_LIST)]
|
||||
self._sheet.delete_genre(index)
|
||||
|
||||
self._toggle_new_genre_state()
|
||||
|
||||
def set_save_sensitivity(self, _):
|
||||
'''
|
||||
action to toggle the state of the save button depending
|
||||
upon the values entered in the genre edit fields
|
||||
'''
|
||||
entry_value = self.genre_entry.get_text()
|
||||
treeiter = self.genre_combobox.get_active_iter()
|
||||
|
||||
entry_value = rb3compat.unicodestr(entry_value, 'utf-8')
|
||||
enable = False
|
||||
try:
|
||||
test = self._iters[(entry_value, self.GENRE_LIST)]
|
||||
if RB.search_fold(self.current_genre) == RB.search_fold(entry_value):
|
||||
# if the current entry is the same then could save
|
||||
enable = True
|
||||
except:
|
||||
# reach here if this is a brand new entry
|
||||
enable = True
|
||||
|
||||
if treeiter == None or entry_value == None or entry_value == "":
|
||||
# no icon chosen, or no entry value then nothing to save
|
||||
enable = False
|
||||
|
||||
self.save_button.set_sensitive(enable)
|
||||
|
||||
def _toggle_new_genre_state(self):
|
||||
'''
|
||||
fire an event - uses gsettings and an object such as a
|
||||
controller connects to receive the signal that a new or amended
|
||||
genre has been made
|
||||
'''
|
||||
gs = GSetting()
|
||||
test = self.settings[gs.PluginKey.NEW_GENRE_ICON]
|
||||
|
||||
if test:
|
||||
test = False
|
||||
else:
|
||||
test = True
|
||||
|
||||
self.settings[gs.PluginKey.NEW_GENRE_ICON] = test
|
||||
|
||||
def on_show_launchpad_toggled(self, button):
|
||||
self.launchpad_label.set_visible(button.get_active())
|
||||
|
||||
|
||||
|
||||
@@ -16,12 +16,16 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
from gi.repository import GdkPixbuf
|
||||
|
||||
from datetime import date
|
||||
from collections import OrderedDict
|
||||
from collections import namedtuple
|
||||
|
||||
from gi.repository import GObject
|
||||
from gi.repository import Gdk
|
||||
from gi.repository import RB
|
||||
|
||||
import rb
|
||||
from gi.repository import Gio
|
||||
from gi.repository import GLib
|
||||
|
||||
from coverart_browser_prefs import CoverLocale
|
||||
from coverart_browser_prefs import GSetting
|
||||
@@ -30,15 +34,23 @@ from coverart_utils import GenreConfiguredSpriteSheet
|
||||
from coverart_utils import ConfiguredSpriteSheet
|
||||
from coverart_utils import get_stock_size
|
||||
from coverart_utils import CaseInsensitiveDict
|
||||
from coverart_utils import Theme
|
||||
import rb
|
||||
|
||||
|
||||
MenuNodeT = namedtuple('MenuNode', 'label menutype typevalue')
|
||||
|
||||
|
||||
def MenuNode(label, menutype=None, typevalue=None):
|
||||
return MenuNodeT(label, menutype, typevalue)
|
||||
|
||||
from datetime import date
|
||||
from collections import OrderedDict
|
||||
|
||||
class OptionsController(GObject.Object):
|
||||
|
||||
# properties
|
||||
options = GObject.property(type=object, default=None)
|
||||
current_key = GObject.property(type=str, default=None)
|
||||
update_image = GObject.property(type=bool, default=False)
|
||||
enabled = GObject.property(type=bool, default=True)
|
||||
|
||||
def __init__(self):
|
||||
super(OptionsController, self).__init__()
|
||||
@@ -66,8 +78,33 @@ class OptionsController(GObject.Object):
|
||||
def get_current_description(self):
|
||||
return self.current_key
|
||||
|
||||
class PlaylistPopupController(OptionsController):
|
||||
def update_images(self, *args):
|
||||
pass
|
||||
|
||||
def create_spritesheet(self, plugin, sheet, typestr):
|
||||
'''
|
||||
helper function to create a specific spritesheet
|
||||
'''
|
||||
if sheet:
|
||||
del sheet
|
||||
|
||||
return ConfiguredSpriteSheet(plugin, typestr, get_stock_size())
|
||||
|
||||
def create_button_image(self, plugin, image, icon_name):
|
||||
'''
|
||||
helper function to create a button image
|
||||
'''
|
||||
if image:
|
||||
del image
|
||||
|
||||
path = 'img/' + Theme(self.plugin).current + '/'
|
||||
|
||||
return create_pixbuf_from_file_at_size(
|
||||
rb.find_plugin_file(self.plugin, path + icon_name),
|
||||
*get_stock_size())
|
||||
|
||||
|
||||
class PlaylistPopupController(OptionsController):
|
||||
def __init__(self, plugin, album_model):
|
||||
super(PlaylistPopupController, self).__init__()
|
||||
|
||||
@@ -85,27 +122,31 @@ class PlaylistPopupController(OptionsController):
|
||||
if " (" in self._queue_name:
|
||||
self._queue_name = self._queue_name[0:self._queue_name.find(" (")]
|
||||
|
||||
# configure the sprite sheet
|
||||
self._spritesheet = ConfiguredSpriteSheet(plugin, 'playlist',
|
||||
get_stock_size())
|
||||
self._spritesheet = None
|
||||
self._update_options(shell)
|
||||
|
||||
# get the playlist manager and it's model
|
||||
playlist_manager = shell.props.playlist_manager
|
||||
playlist_model = playlist_manager.props.display_page_model
|
||||
# get the playlist model so we can monitor changes
|
||||
playlist_model = shell.props.display_page_model
|
||||
|
||||
# connect signals to update playlists
|
||||
playlist_model.connect('row-inserted', self._update_options, shell)
|
||||
playlist_model.connect('row-deleted', self._update_options, shell)
|
||||
playlist_model.connect('row-changed', self._update_options, shell)
|
||||
|
||||
# generate initial options
|
||||
self._update_options(shell)
|
||||
def update_images(self, *args):
|
||||
self._spritesheet = self.create_spritesheet(self.plugin,
|
||||
self._spritesheet, 'playlist')
|
||||
|
||||
if args[-1]:
|
||||
self.update_image = True
|
||||
|
||||
def _update_options(self, *args):
|
||||
shell = args[-1]
|
||||
self.update_images(False)
|
||||
|
||||
playlist_manager = shell.props.playlist_manager
|
||||
still_exists = self.current_key == self._library_name or\
|
||||
self.current_key == self._queue_name
|
||||
still_exists = self.current_key == self._library_name or \
|
||||
self.current_key == self._queue_name
|
||||
|
||||
# retrieve the options
|
||||
values = OrderedDict()
|
||||
@@ -123,11 +164,11 @@ class PlaylistPopupController(OptionsController):
|
||||
values[name] = playlist
|
||||
|
||||
still_exists = still_exists or name == self.current_key
|
||||
|
||||
self.values = values
|
||||
self.options = values.keys()
|
||||
|
||||
self.current_key = self.current_key if still_exists else\
|
||||
self.values = values
|
||||
self.options = list(values.keys())
|
||||
|
||||
self.current_key = self.current_key if still_exists else \
|
||||
self._library_name
|
||||
|
||||
def do_action(self):
|
||||
@@ -137,7 +178,7 @@ class PlaylistPopupController(OptionsController):
|
||||
self._album_model.remove_filter('model')
|
||||
else:
|
||||
self._album_model.replace_filter('model',
|
||||
playlist.get_query_model())
|
||||
playlist.get_query_model())
|
||||
|
||||
def get_current_image(self):
|
||||
playlist = self.values[self.current_key]
|
||||
@@ -155,13 +196,15 @@ class PlaylistPopupController(OptionsController):
|
||||
|
||||
|
||||
class GenrePopupController(OptionsController):
|
||||
# properties
|
||||
new_genre_icon = GObject.property(type=bool, default=False)
|
||||
|
||||
def __init__(self, plugin, album_model):
|
||||
super(GenrePopupController, self).__init__()
|
||||
|
||||
cl = CoverLocale()
|
||||
cl.switch_locale(cl.Locale.LOCALE_DOMAIN)
|
||||
|
||||
|
||||
self._album_model = album_model
|
||||
|
||||
shell = plugin.shell
|
||||
@@ -169,34 +212,57 @@ class GenrePopupController(OptionsController):
|
||||
|
||||
# create a new property model for the genres
|
||||
genres_model = RB.RhythmDBPropertyModel.new(shell.props.db,
|
||||
RB.RhythmDBPropType.GENRE)
|
||||
RB.RhythmDBPropType.GENRE)
|
||||
|
||||
query = shell.props.library_source.props.base_query_model
|
||||
genres_model.props.query_model = query
|
||||
|
||||
# initial genre
|
||||
self._initial_genre = _('All Genres')#genres_model[0][0]
|
||||
self._initial_genre = _('All Genres')
|
||||
|
||||
# initialise the button spritesheet and other images
|
||||
self._spritesheet = GenreConfiguredSpriteSheet(plugin, 'genre',
|
||||
get_stock_size())
|
||||
self._default_image = create_pixbuf_from_file_at_size(
|
||||
rb.find_plugin_file(plugin, 'img/default_genre.png'),
|
||||
*get_stock_size())
|
||||
self._unrecognised_image = create_pixbuf_from_file_at_size(
|
||||
rb.find_plugin_file(plugin, 'img/unrecognised_genre.png'),
|
||||
*get_stock_size())
|
||||
self._spritesheet = None
|
||||
self._default_image = None
|
||||
self._unrecognised_image = None
|
||||
|
||||
# connect signals to update genres
|
||||
query.connect('row-inserted', self._update_options, genres_model)
|
||||
query.connect('row-deleted', self._update_options, genres_model)
|
||||
query.connect('row-changed', self._update_options, genres_model)
|
||||
self._connect_properties()
|
||||
self._connect_signals(query, genres_model)
|
||||
|
||||
# generate initial popup
|
||||
self._update_options(genres_model)
|
||||
|
||||
def update_images(self, *args):
|
||||
if self._spritesheet:
|
||||
del self._spritesheet
|
||||
|
||||
self._spritesheet = GenreConfiguredSpriteSheet(self.plugin,
|
||||
'genre', get_stock_size())
|
||||
self._default_image = self.create_button_image(self.plugin,
|
||||
self._default_image, 'default_genre.png')
|
||||
self._unrecognised_image = self.create_button_image(self.plugin,
|
||||
self._unrecognised_image, 'unrecognised_genre.png')
|
||||
|
||||
if args[-1]:
|
||||
self.update_image = True
|
||||
|
||||
def _connect_signals(self, query, genres_model):
|
||||
# connect signals to update genres
|
||||
self.connect('notify::new-genre-icon', self._update_options, genres_model)
|
||||
query.connect('row-inserted', self._update_options, genres_model)
|
||||
query.connect('row-deleted', self._update_options, genres_model)
|
||||
query.connect('row-changed', self._update_options, genres_model)
|
||||
|
||||
def _connect_properties(self):
|
||||
gs = GSetting()
|
||||
setting = gs.get_setting(gs.Path.PLUGIN)
|
||||
|
||||
setting.bind(gs.PluginKey.NEW_GENRE_ICON, self, 'new_genre_icon',
|
||||
Gio.SettingsBindFlags.GET)
|
||||
|
||||
def _update_options(self, *args):
|
||||
genres_model = args[-1]
|
||||
|
||||
self.update_images(False)
|
||||
|
||||
still_exists = False
|
||||
|
||||
# retrieve the options
|
||||
@@ -204,18 +270,20 @@ class GenrePopupController(OptionsController):
|
||||
row_num = 0
|
||||
for row in genres_model:
|
||||
if row_num == 0:
|
||||
cl = CoverLocale()
|
||||
cl.switch_locale(cl.Locale.LOCALE_DOMAIN)
|
||||
genre = _('All Genres')
|
||||
row_num = row_num + 1
|
||||
else:
|
||||
genre = row[0]
|
||||
|
||||
|
||||
options.append(genre)
|
||||
|
||||
still_exists = still_exists or genre == self.current_key
|
||||
|
||||
self.options = options
|
||||
|
||||
self.current_key = self.current_key if still_exists else\
|
||||
self.current_key = self.current_key if still_exists else \
|
||||
self._initial_genre
|
||||
|
||||
def do_action(self):
|
||||
@@ -233,58 +301,77 @@ class GenrePopupController(OptionsController):
|
||||
|
||||
if test_genre == self._initial_genre.lower():
|
||||
image = self._default_image
|
||||
elif test_genre in self._spritesheet:
|
||||
image = self._spritesheet[test_genre]
|
||||
else:
|
||||
image = self._find_alternates(test_genre)
|
||||
|
||||
if image == self._unrecognised_image and \
|
||||
test_genre in self._spritesheet:
|
||||
image = self._spritesheet[test_genre]
|
||||
|
||||
return image
|
||||
|
||||
def _find_alternates(self, test_genre):
|
||||
# the following genre checks are required
|
||||
# 1. if we have locale specific genres check first
|
||||
# 2. then check locale specific alternates
|
||||
# 3. then check if we have default genres
|
||||
# 4. then check if we have default alternates
|
||||
# 1. if we have user defined genres
|
||||
# 2. then check locale specific system genres
|
||||
# 3. then check local specific alternates
|
||||
# 4. then check if we system genres
|
||||
|
||||
|
||||
# first check if any of the locale genres are a substring
|
||||
# where necessary check if any of the genres are a substring
|
||||
# of test_genre - check in reverse order so that we
|
||||
# test largest strings first (prevents spurious matches with
|
||||
# short strings)
|
||||
# N.B. we use RB.search_fold since the strings can be
|
||||
# in a mixture of cases, both unicode (normalized or not) and str
|
||||
# and as usual python cannot mix and match these types.
|
||||
|
||||
|
||||
|
||||
test_genre = RB.search_fold(test_genre)
|
||||
|
||||
ret, sprite = self._match_genres(test_genre, self._spritesheet.GENRE_USER)
|
||||
if ret:
|
||||
return sprite
|
||||
|
||||
for genre in sorted(self._spritesheet.locale_names,
|
||||
key=lambda b: (-len(b), b)):
|
||||
if RB.search_fold(genre) in RB.search_fold(test_genre):
|
||||
key=lambda b: (-len(b), b)):
|
||||
if RB.search_fold(genre) in test_genre:
|
||||
return self._spritesheet[self._spritesheet.locale_names[genre]]
|
||||
|
||||
# next check locale alternates
|
||||
case_search = CaseInsensitiveDict(self._spritesheet.locale_alternate)
|
||||
|
||||
if RB.search_fold(test_genre) in case_search:
|
||||
return self._spritesheet[case_search[RB.search_fold(test_genre)]]
|
||||
ret, sprite = self._match_genres(test_genre, self._spritesheet.GENRE_LOCALE)
|
||||
if ret:
|
||||
return sprite
|
||||
|
||||
ret, sprite = self._match_genres(test_genre, self._spritesheet.GENRE_SYSTEM)
|
||||
if ret:
|
||||
return sprite
|
||||
|
||||
# check if any of the default genres are a substring
|
||||
# of test_genre - check in reverse order so that we
|
||||
# test largest strings first (prevents spurious matches with
|
||||
# short strings)
|
||||
for genre in sorted(self._spritesheet.names,
|
||||
key=lambda b: (-len(b), b)):
|
||||
if RB.search_fold(genre) in RB.search_fold(test_genre):
|
||||
key=lambda b: (-len(b), b)):
|
||||
if RB.search_fold(genre) in test_genre:
|
||||
return self._spritesheet[genre]
|
||||
|
||||
# next check alternates
|
||||
case_search = CaseInsensitiveDict(self._spritesheet.alternate)
|
||||
if RB.search_fold(test_genre) in case_search:
|
||||
return self._spritesheet[case_search[RB.search_fold(test_genre)]]
|
||||
|
||||
# if no matches then default to unrecognised image
|
||||
return self._unrecognised_image
|
||||
|
||||
def _match_genres(self, test_genre, genre_type):
|
||||
case_search = CaseInsensitiveDict(
|
||||
dict((k.name, v) for k, v in self._spritesheet.genre_alternate.items()
|
||||
if k.genre_type == genre_type))
|
||||
|
||||
if test_genre in case_search:
|
||||
return (True, self._spritesheet[case_search[test_genre]])
|
||||
else:
|
||||
return (False, None)
|
||||
|
||||
|
||||
def get_current_description(self):
|
||||
cl = CoverLocale()
|
||||
cl.switch_locale(cl.Locale.LOCALE_DOMAIN)
|
||||
if self.current_key == self._initial_genre:
|
||||
return _('All Genres')
|
||||
else:
|
||||
@@ -292,35 +379,40 @@ class GenrePopupController(OptionsController):
|
||||
|
||||
|
||||
class SortPopupController(OptionsController):
|
||||
|
||||
def __init__(self, plugin, album_model):
|
||||
def __init__(self, plugin, viewmgr):
|
||||
super(SortPopupController, self).__init__()
|
||||
|
||||
self._album_model = album_model
|
||||
|
||||
# initialise spritesheet
|
||||
self._spritesheet = ConfiguredSpriteSheet(plugin, 'sort',
|
||||
get_stock_size())
|
||||
|
||||
self._viewmgr = viewmgr
|
||||
self.plugin = plugin
|
||||
# sorts dictionary
|
||||
cl = CoverLocale()
|
||||
cl.switch_locale(cl.Locale.LOCALE_DOMAIN)
|
||||
|
||||
self.values = OrderedDict([(_('Sort by album name'), 'name'),
|
||||
(_('Sort by album artist'), 'artist'),
|
||||
(_('Sort by year'), 'year'),
|
||||
(_('Sort by rating'), 'rating')])
|
||||
(_('Sort by album artist'), 'artist'),
|
||||
(_('Sort by year'), 'year'),
|
||||
(_('Sort by rating'), 'rating')])
|
||||
|
||||
self.options = self.values.keys()
|
||||
self.options = list(self.values.keys())
|
||||
|
||||
# get the current sort key and initialise the superclass
|
||||
gs = GSetting()
|
||||
source_settings = gs.get_setting(gs.Path.PLUGIN)
|
||||
value = source_settings[gs.PluginKey.SORT_BY]
|
||||
|
||||
self.current_key = self.values.keys()[
|
||||
self.values.values().index(value)]
|
||||
|
||||
self._spritesheet = None
|
||||
self.update_images(False)
|
||||
|
||||
self.current_key = list(self.values.keys())[
|
||||
list(self.values.values()).index(value)]
|
||||
|
||||
def update_images(self, *args):
|
||||
self._spritesheet = self.create_spritesheet(self.plugin,
|
||||
self._spritesheet, 'sort')
|
||||
|
||||
if args[-1]:
|
||||
self.update_image = True
|
||||
|
||||
def do_action(self):
|
||||
sort = self.values[self.current_key]
|
||||
|
||||
@@ -328,55 +420,191 @@ class SortPopupController(OptionsController):
|
||||
settings = gs.get_setting(gs.Path.PLUGIN)
|
||||
settings[gs.PluginKey.SORT_BY] = sort
|
||||
|
||||
self._album_model.sort(sort)
|
||||
self._viewmgr.current_view.get_default_manager().emit('sort', "album")
|
||||
|
||||
def get_current_image(self):
|
||||
sort = self.values[self.current_key]
|
||||
sort = self.values[self.current_key]
|
||||
return self._spritesheet[sort]
|
||||
|
||||
|
||||
class DecadePopupController(OptionsController):
|
||||
class ArtistSortPopupController(OptionsController):
|
||||
def __init__(self, plugin, viewmgr):
|
||||
super(ArtistSortPopupController, self).__init__()
|
||||
|
||||
self._viewmgr = viewmgr
|
||||
self.plugin = plugin
|
||||
# sorts dictionary
|
||||
cl = CoverLocale()
|
||||
cl.switch_locale(cl.Locale.LOCALE_DOMAIN)
|
||||
|
||||
self.values = OrderedDict([(_('Sort by album name'), 'name_artist'),
|
||||
(_('Sort by year'), 'year_artist'),
|
||||
(_('Sort by rating'), 'rating_artist')])
|
||||
|
||||
self.options = list(self.values.keys())
|
||||
|
||||
# get the current sort key and initialise the superclass
|
||||
gs = GSetting()
|
||||
source_settings = gs.get_setting(gs.Path.PLUGIN)
|
||||
value = source_settings[gs.PluginKey.SORT_BY_ARTIST]
|
||||
|
||||
if value not in list(self.values.values()):
|
||||
print("here")
|
||||
value = 'name_artist'
|
||||
source_settings[gs.PluginKey.SORT_BY_ARTIST] = value
|
||||
|
||||
self._spritesheet = None
|
||||
self.update_images(False)
|
||||
|
||||
self.current_key = list(self.values.keys())[
|
||||
list(self.values.values()).index(value)]
|
||||
print(self.current_key)
|
||||
|
||||
def update_images(self, *args):
|
||||
self._spritesheet = self.create_spritesheet(self.plugin,
|
||||
self._spritesheet, 'sort_artist')
|
||||
|
||||
if args[-1]:
|
||||
self.update_image = True
|
||||
|
||||
def do_action(self):
|
||||
sort = self.values[self.current_key]
|
||||
|
||||
gs = GSetting()
|
||||
settings = gs.get_setting(gs.Path.PLUGIN)
|
||||
settings[gs.PluginKey.SORT_BY_ARTIST] = sort
|
||||
|
||||
self._viewmgr.current_view.get_default_manager().emit('sort', "artist")
|
||||
|
||||
def get_current_image(self):
|
||||
sort = self.values[self.current_key]
|
||||
return self._spritesheet[sort]
|
||||
|
||||
|
||||
class PropertiesMenuController(OptionsController):
|
||||
favourites = GObject.property(type=bool, default=False)
|
||||
follow = GObject.property(type=bool, default=False)
|
||||
|
||||
def __init__(self, plugin, source):
|
||||
super(PropertiesMenuController, self).__init__()
|
||||
|
||||
self._source = source
|
||||
self.plugin = plugin
|
||||
self._connect_properties()
|
||||
# sorts dictionary
|
||||
cl = CoverLocale()
|
||||
cl.switch_locale(cl.Locale.LOCALE_DOMAIN)
|
||||
# options
|
||||
self.values = OrderedDict()
|
||||
self.values[MenuNode(_('Download all covers'))] = 'download'
|
||||
self.values[MenuNode(_('Play random album'))] = 'random'
|
||||
self.values[MenuNode(_('Follow playing song'), 'check',
|
||||
(True if self.follow else False))] = 'follow'
|
||||
self.values[MenuNode('separator1', 'separator')] = ''
|
||||
self.values[MenuNode(_('Use favourites only'), 'check',
|
||||
(True if self.favourites else False))] = 'favourite'
|
||||
self.values[MenuNode('separator2', 'separator')] = ''
|
||||
self.values[MenuNode(_('Browser Preferences'))] = 'browser prefs'
|
||||
self.values[MenuNode(_('Search Preferences'))] = 'search prefs'
|
||||
|
||||
self.options = list(self.values.keys())
|
||||
|
||||
self.update_images(False)
|
||||
|
||||
if self.favourites:
|
||||
self._source.propertiesbutton_callback('favourite')
|
||||
|
||||
if self.follow:
|
||||
self._source.propertiesbutton_callback('follow')
|
||||
|
||||
self.current_key = None
|
||||
|
||||
def _connect_properties(self):
|
||||
gs = GSetting()
|
||||
setting = gs.get_setting(gs.Path.PLUGIN)
|
||||
setting.bind(
|
||||
gs.PluginKey.USE_FAVOURITES,
|
||||
self,
|
||||
'favourites',
|
||||
Gio.SettingsBindFlags.DEFAULT)
|
||||
setting.bind(
|
||||
gs.PluginKey.FOLLOWING,
|
||||
self,
|
||||
'follow',
|
||||
Gio.SettingsBindFlags.DEFAULT)
|
||||
|
||||
def _change_key(self, dict, old, new):
|
||||
for i in range(len(dict)):
|
||||
k, v = dict.popitem(False)
|
||||
dict[new if old == k else k] = v
|
||||
|
||||
def update_images(self, *args):
|
||||
self._image = self.create_button_image(self.plugin,
|
||||
None, 'properties.png')
|
||||
|
||||
if args[-1]:
|
||||
self.update_image = True
|
||||
|
||||
def do_action(self):
|
||||
if self.current_key:
|
||||
key = [node for node in self.values if node.label == self.current_key]
|
||||
|
||||
if self.current_key == _('Use favourites only'):
|
||||
self.favourites = not self.favourites
|
||||
|
||||
if self.current_key == _('Follow playing song'):
|
||||
self.follow = not self.follow
|
||||
|
||||
self._source.propertiesbutton_callback(self.values[key[0]])
|
||||
self.current_key = None
|
||||
|
||||
def get_current_image(self):
|
||||
return self._image
|
||||
|
||||
def get_current_description(self):
|
||||
return _('Properties')
|
||||
|
||||
|
||||
class DecadePopupController(OptionsController):
|
||||
def __init__(self, plugin, album_model):
|
||||
super(DecadePopupController, self).__init__()
|
||||
|
||||
self._album_model = album_model
|
||||
self.plugin = plugin
|
||||
|
||||
# initialize spritesheet
|
||||
self._spritesheet = ConfiguredSpriteSheet(plugin, 'decade',
|
||||
get_stock_size())
|
||||
self._spritesheet = None
|
||||
|
||||
# decade options
|
||||
cl = CoverLocale()
|
||||
cl.switch_locale(cl.Locale.LOCALE_DOMAIN)
|
||||
|
||||
self.values = OrderedDict()
|
||||
|
||||
self.values[ _('All Decades') ] = [-1, 'All Decades']
|
||||
#'20s' as in the decade 2010
|
||||
self.values[ _('20s') ] = [2020, '20s']
|
||||
#'10s' as in the decade 2010
|
||||
self.values[ _('10s') ] = [2010, '10s']
|
||||
#'00s' as in the decade 2000
|
||||
self.values[ _('00s') ] = [2000, '00s']
|
||||
#'90s' as in the decade 1990
|
||||
self.values[ _('90s') ] = [1990, '90s']
|
||||
#'80s' as in the decade 1980
|
||||
self.values[ _('80s') ] = [1980, '80s']
|
||||
#'70s' as in the decade 1970
|
||||
self.values[ _('70s') ] = [1970, '70s']
|
||||
#'60s' as in the decade 1960
|
||||
self.values[ _('60s') ] = [1960, '60s']
|
||||
#'50s' as in the decade 1950
|
||||
self.values[ _('50s') ] = [1950, '50s']
|
||||
#'40s' as in the decade 1940
|
||||
self.values[ _('40s') ] = [1940, '40s']
|
||||
#'30s' as in the decade 1930
|
||||
self.values[ _('30s') ] = [1930, '30s']
|
||||
#'Older' as in 'older than the year 1930'
|
||||
self.values[ _('Older') ] = [-1, 'Older']
|
||||
|
||||
self.options = self.values.keys()
|
||||
self.values[_('All Decades')] = [-1, 'All Decades']
|
||||
# '20s' as in the decade 2010
|
||||
self.values[_('20s')] = [2020, '20s']
|
||||
#'10s' as in the decade 2010
|
||||
self.values[_('10s')] = [2010, '10s']
|
||||
#'00s' as in the decade 2000
|
||||
self.values[_('00s')] = [2000, '00s']
|
||||
#'90s' as in the decade 1990
|
||||
self.values[_('90s')] = [1990, '90s']
|
||||
#'80s' as in the decade 1980
|
||||
self.values[_('80s')] = [1980, '80s']
|
||||
#'70s' as in the decade 1970
|
||||
self.values[_('70s')] = [1970, '70s']
|
||||
#'60s' as in the decade 1960
|
||||
self.values[_('60s')] = [1960, '60s']
|
||||
#'50s' as in the decade 1950
|
||||
self.values[_('50s')] = [1950, '50s']
|
||||
#'40s' as in the decade 1940
|
||||
self.values[_('40s')] = [1940, '40s']
|
||||
#'30s' as in the decade 1930
|
||||
self.values[_('30s')] = [1930, '30s']
|
||||
#'Older' as in 'older than the year 1930'
|
||||
self.values[_('Older')] = [-1, 'Older']
|
||||
|
||||
self.options = list(self.values.keys())
|
||||
|
||||
# if we aren't on the 20s yet, remove it
|
||||
if date.today().year < 2020:
|
||||
@@ -384,14 +612,23 @@ class DecadePopupController(OptionsController):
|
||||
|
||||
# define a initial decade an set the initial key
|
||||
self._initial_decade = self.options[0]
|
||||
self.update_images(False)
|
||||
|
||||
self.current_key = self._initial_decade
|
||||
|
||||
|
||||
def update_images(self, *args):
|
||||
self._spritesheet = self.create_spritesheet(self.plugin,
|
||||
self._spritesheet, 'decade')
|
||||
|
||||
if args[-1]:
|
||||
self.update_image = True
|
||||
|
||||
def do_action(self):
|
||||
if self.current_key == self._initial_decade:
|
||||
self._album_model.remove_filter('decade')
|
||||
else:
|
||||
self._album_model.replace_filter('decade',
|
||||
self.values[self.current_key][0])
|
||||
self.values[self.current_key][0])
|
||||
|
||||
def get_current_image(self):
|
||||
decade = self.values[self.current_key][1]
|
||||
@@ -402,49 +639,66 @@ class DecadePopupController(OptionsController):
|
||||
|
||||
|
||||
class SortOrderToggleController(OptionsController):
|
||||
toolbar_type = "album"
|
||||
|
||||
def __init__(self, plugin, album_model):
|
||||
def __init__(self, plugin, viewmgr):
|
||||
super(SortOrderToggleController, self).__init__()
|
||||
|
||||
self._album_model = album_model
|
||||
self._viewmgr = viewmgr
|
||||
self.plugin = plugin
|
||||
|
||||
# options
|
||||
self.values = OrderedDict([(_('Sort in descending order'), False),
|
||||
(_('Sort in ascending order'), True)])
|
||||
self.options = self.values.keys()
|
||||
(_('Sort in ascending order'), True)])
|
||||
self.options = list(self.values.keys())
|
||||
|
||||
# initialize images
|
||||
self._images = []
|
||||
self._images.append(GdkPixbuf.Pixbuf.new_from_file_at_size(
|
||||
rb.find_plugin_file(plugin, 'img/arrow_down.png'),
|
||||
*get_stock_size()))
|
||||
self._images.append(GdkPixbuf.Pixbuf.new_from_file_at_size(
|
||||
rb.find_plugin_file(plugin, 'img/arrow_up.png'),
|
||||
*get_stock_size()))
|
||||
|
||||
# set the current key
|
||||
self.gs = GSetting()
|
||||
self.settings = self.gs.get_setting(self.gs.Path.PLUGIN)
|
||||
sort_order = self.settings[self.gs.PluginKey.SORT_ORDER]
|
||||
self.key = self.get_key()
|
||||
sort_order = self.settings[self.key]
|
||||
self.current_key = list(self.values.keys())[
|
||||
list(self.values.values()).index(sort_order)]
|
||||
self.update_images(False)
|
||||
|
||||
self.current_key = self.values.keys()[
|
||||
self.values.values().index(sort_order)]
|
||||
def get_key(self):
|
||||
return self.gs.PluginKey.SORT_ORDER
|
||||
|
||||
def update_images(self, *args):
|
||||
# initialize images
|
||||
if len(self._images) > 0:
|
||||
del self._images[:]
|
||||
|
||||
self._images.append(self.create_button_image(self.plugin,
|
||||
None, 'arrow_down.png'))
|
||||
self._images.append(self.create_button_image(self.plugin,
|
||||
None, 'arrow_up.png'))
|
||||
|
||||
if args[-1]:
|
||||
self.update_image = True
|
||||
|
||||
def do_action(self):
|
||||
sort_order = self.values[self.current_key]
|
||||
|
||||
if not sort_order or\
|
||||
sort_order != self.settings[self.gs.PluginKey.SORT_ORDER]:
|
||||
self._album_model.sort(reverse=True)
|
||||
|
||||
self.settings[self.gs.PluginKey.SORT_ORDER] = sort_order
|
||||
self.settings[self.key] = sort_order
|
||||
self._viewmgr.current_view.get_default_manager().emit('sort', self.toolbar_type)
|
||||
|
||||
def get_current_image(self):
|
||||
return self._images[self.get_current_key_index()]
|
||||
|
||||
|
||||
class AlbumSearchEntryController(OptionsController):
|
||||
class ArtistSortOrderToggleController(SortOrderToggleController):
|
||||
toolbar_type = "artist"
|
||||
|
||||
def __init__(self, plugin, model):
|
||||
super(ArtistSortOrderToggleController, self).__init__(plugin, model)
|
||||
|
||||
def get_key(self):
|
||||
return self.gs.PluginKey.SORT_ORDER_ARTIST
|
||||
|
||||
|
||||
class AlbumSearchEntryController(OptionsController):
|
||||
# properties
|
||||
search_text = GObject.property(type=str, default='')
|
||||
|
||||
@@ -459,22 +713,47 @@ class AlbumSearchEntryController(OptionsController):
|
||||
self.values[_('Search all fields')] = 'all'
|
||||
self.values[_('Search album artists')] = 'album_artist'
|
||||
self.values[_('Search track artists')] = 'artist'
|
||||
self.values[_('Search composers')] = 'composers'
|
||||
self.values[_('Search albums')] = 'album_name'
|
||||
self.values[_('Search tracks')] = 'track'
|
||||
self.values[_('Search titles')] = 'track'
|
||||
|
||||
self.options = self.values.keys()
|
||||
self.current_key = self.values.keys()[0]
|
||||
self.options = list(self.values.keys())
|
||||
self.current_key = list(self.values.keys())[0]
|
||||
|
||||
self._typing = False
|
||||
self._typing_counter = 0
|
||||
self._current_search_text = ""
|
||||
|
||||
def do_action(self):
|
||||
# remove old filter
|
||||
self._album_model.remove_filter(self._filter_type, False)
|
||||
|
||||
# asign the new filter
|
||||
# assign the new filter
|
||||
self._filter_type = self.values[self.current_key]
|
||||
|
||||
self.do_search(self.search_text, True)
|
||||
|
||||
def _search_typing(self, *args):
|
||||
self._typing_counter = self._typing_counter + 1
|
||||
|
||||
if self._typing_counter >= 4 and self._typing:
|
||||
self._typing = False
|
||||
self._change_filter(self._current_search_text, False)
|
||||
|
||||
return self._typing
|
||||
|
||||
def _change_filter(self, search_text, force):
|
||||
# self.search_text = search_text
|
||||
self._current_search_text = search_text
|
||||
|
||||
if search_text:
|
||||
self._album_model.replace_filter(self._filter_type,
|
||||
search_text)
|
||||
elif not force:
|
||||
self._album_model.remove_filter(self._filter_type)
|
||||
|
||||
def do_search(self, search_text, force=False):
|
||||
'''
|
||||
if self.search_text != search_text or force:
|
||||
self.search_text = search_text
|
||||
|
||||
@@ -484,11 +763,29 @@ class AlbumSearchEntryController(OptionsController):
|
||||
elif not force:
|
||||
self._album_model.remove_filter(self._filter_type)
|
||||
|
||||
'''
|
||||
# self.search_text = search_text
|
||||
if force:
|
||||
self._typing_counter = 99
|
||||
self._typing = False
|
||||
self._change_filter(search_text, force)
|
||||
return
|
||||
|
||||
if self._current_search_text != search_text:
|
||||
|
||||
#self.search_text = search_text
|
||||
self._current_search_text = search_text
|
||||
self._typing_counter = 0
|
||||
|
||||
if not self._typing:
|
||||
self._typing = True
|
||||
|
||||
Gdk.threads_add_timeout(GLib.PRIORITY_DEFAULT_IDLE, 100,
|
||||
self._search_typing)
|
||||
|
||||
|
||||
class AlbumQuickSearchController(object):
|
||||
|
||||
def __init__(self, source, album_manager):
|
||||
self._source = source
|
||||
def __init__(self, album_manager):
|
||||
self._album_manager = album_manager
|
||||
|
||||
def connect_quick_search(self, quick_search):
|
||||
@@ -498,15 +795,14 @@ class AlbumQuickSearchController(object):
|
||||
|
||||
def _on_quick_search(self, quick_search, search_text, *args):
|
||||
album = self._album_manager.model.find_first_visible('album_name',
|
||||
search_text)
|
||||
search_text)
|
||||
|
||||
if album:
|
||||
self._source.select_album(album)
|
||||
#self._album_manager.cover_view.select_album(album)
|
||||
path = self._album_manager.model.get_path(album)
|
||||
self._album_manager.current_view.select_and_scroll_to_path(path)
|
||||
|
||||
def _on_arrow_pressed(self, quick_search, key, *args):
|
||||
current = self._source.get_selected_albums()[0]
|
||||
#current = self.album_manager.cover_view.get_selected_albums()[0]
|
||||
current = self._album_manager.current_view.get_selected_objects()[0]
|
||||
search_text = quick_search.get_text()
|
||||
album = None
|
||||
|
||||
@@ -518,8 +814,36 @@ class AlbumQuickSearchController(object):
|
||||
'album_name', search_text, current)
|
||||
|
||||
if album:
|
||||
self._source.select_album(album)
|
||||
#self._album_manager.cover_view.select_album(album)
|
||||
path = self._album_manager.model.get_path(album)
|
||||
self._album_manager.current_view.select_and_scroll_to_path(path)
|
||||
|
||||
def _on_hide(self, quick_search, *args):
|
||||
self._album_manager.cover_view.grab_focus()
|
||||
self._album_manager.current_view.grab_focus()
|
||||
|
||||
|
||||
class ViewController(OptionsController):
|
||||
def __init__(self, shell, viewmgr):
|
||||
super(ViewController, self).__init__()
|
||||
|
||||
self._viewmgr = viewmgr
|
||||
|
||||
from coverart_browser_source import Views
|
||||
|
||||
views = Views(shell)
|
||||
|
||||
self.values = OrderedDict()
|
||||
for view_name in views.get_view_names():
|
||||
self.values[views.get_menu_name(view_name)] = view_name
|
||||
print(view_name)
|
||||
|
||||
self.options = list(self.values.keys())
|
||||
viewmgr.connect('new-view', self.on_notify_view_name)
|
||||
|
||||
def on_notify_view_name(self, *args):
|
||||
for key in self.options:
|
||||
if self.values[key] == self._viewmgr.view_name:
|
||||
self.current_key = key
|
||||
|
||||
def do_action(self):
|
||||
if self._viewmgr.view_name != self.values[self.current_key]:
|
||||
self._viewmgr.view_name = self.values[self.current_key]
|
||||
|
||||
@@ -0,0 +1,434 @@
|
||||
# -*- Mode: python; coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*-
|
||||
#
|
||||
# Copyright (C) 2012 - fossfreedom
|
||||
# Copyright (C) 2012 - Agustin Carrasco
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
import json
|
||||
import os
|
||||
from xml.sax.saxutils import escape
|
||||
|
||||
from gi.repository import Gdk
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import GLib
|
||||
from gi.repository import GObject
|
||||
from gi.repository import Gio
|
||||
|
||||
from coverart_browser_prefs import GSetting
|
||||
from coverart_browser_prefs import webkit_support
|
||||
from coverart_widgets import AbstractView
|
||||
from coverart_widgets import PanedCollapsible
|
||||
import rb
|
||||
|
||||
|
||||
class FlowShowingPolicy(GObject.Object):
|
||||
'''
|
||||
Policy that mostly takes care of how and when things should be showed on
|
||||
the view that makes use of the `AlbumsModel`.
|
||||
'''
|
||||
|
||||
def __init__(self, flow_view):
|
||||
super(FlowShowingPolicy, self).__init__()
|
||||
|
||||
self._flow_view = flow_view
|
||||
self.counter = 0
|
||||
self._has_initialised = False
|
||||
|
||||
def initialise(self, album_manager):
|
||||
if self._has_initialised:
|
||||
return
|
||||
|
||||
self._has_initialised = True
|
||||
self._album_manager = album_manager
|
||||
self._model = album_manager.model
|
||||
|
||||
|
||||
class CoverFlowView(AbstractView):
|
||||
__gtype_name__ = "CoverFlowView"
|
||||
|
||||
name = 'coverflowview'
|
||||
|
||||
# properties
|
||||
flow_background = GObject.property(type=str, default='W')
|
||||
flow_automatic = GObject.property(type=bool, default=False)
|
||||
flow_scale = GObject.property(type=int, default=100)
|
||||
flow_hide = GObject.property(type=bool, default=False)
|
||||
flow_width = GObject.property(type=int, default=600)
|
||||
flow_appearance = GObject.property(type=str, default='coverflow')
|
||||
flow_max = GObject.property(type=int, default=100)
|
||||
panedposition = PanedCollapsible.Paned.EXPAND
|
||||
|
||||
def __init__(self):
|
||||
super(CoverFlowView, self).__init__()
|
||||
|
||||
self.show_policy = FlowShowingPolicy(self)
|
||||
if webkit_support():
|
||||
from gi.repository import WebKit
|
||||
|
||||
self.view = WebKit.WebView()
|
||||
else:
|
||||
self.view = None
|
||||
|
||||
self._last_album = None
|
||||
self._has_initialised = False
|
||||
self._filter_changed_inprogress = False
|
||||
self._on_first_use = True
|
||||
|
||||
def _connect_properties(self):
|
||||
gs = GSetting()
|
||||
settings = gs.get_setting(gs.Path.PLUGIN)
|
||||
settings.bind(gs.PluginKey.FLOW_APPEARANCE, self,
|
||||
'flow_appearance', Gio.SettingsBindFlags.GET)
|
||||
settings.bind(gs.PluginKey.FLOW_HIDE_CAPTION, self,
|
||||
'flow_hide', Gio.SettingsBindFlags.GET)
|
||||
settings.bind(gs.PluginKey.FLOW_SCALE, self,
|
||||
'flow_scale', Gio.SettingsBindFlags.GET)
|
||||
settings.bind(gs.PluginKey.FLOW_AUTOMATIC, self,
|
||||
'flow_automatic', Gio.SettingsBindFlags.GET)
|
||||
settings.bind(gs.PluginKey.FLOW_BACKGROUND_COLOUR, self,
|
||||
'flow_background', Gio.SettingsBindFlags.GET)
|
||||
settings.bind(gs.PluginKey.FLOW_WIDTH, self,
|
||||
'flow_width', Gio.SettingsBindFlags.GET)
|
||||
settings.bind(gs.PluginKey.FLOW_MAX, self,
|
||||
'flow_max', Gio.SettingsBindFlags.GET)
|
||||
|
||||
def _connect_signals(self, source):
|
||||
self.connect('notify::flow-background',
|
||||
self.filter_changed)
|
||||
self.connect('notify::flow-scale',
|
||||
self.filter_changed)
|
||||
self.connect('notify::flow-hide',
|
||||
self.filter_changed)
|
||||
self.connect('notify::flow-width',
|
||||
self.filter_changed)
|
||||
self.connect('notify::flow-appearance',
|
||||
self.filter_changed)
|
||||
self.connect('notify::flow-max',
|
||||
self.filter_changed)
|
||||
|
||||
def filter_changed(self, *args):
|
||||
# we can get several filter_changed calls per second
|
||||
# lets simplify the processing & potential flickering when the
|
||||
# call to this method has slowed stopped
|
||||
|
||||
self._filter_changed_event = True
|
||||
|
||||
if self._filter_changed_inprogress:
|
||||
return
|
||||
|
||||
self._filter_changed_inprogress = True
|
||||
|
||||
def filter_events(*args):
|
||||
if not self._filter_changed_event:
|
||||
self._filter_changed()
|
||||
self._filter_changed_inprogress = False
|
||||
else:
|
||||
self._filter_changed_event = False
|
||||
return True
|
||||
|
||||
GLib.timeout_add(250, filter_events, None)
|
||||
|
||||
|
||||
def _filter_changed(self, *args):
|
||||
path = rb.find_plugin_file(self.plugin, 'coverflow/index.html')
|
||||
f = open(path)
|
||||
string = f.read()
|
||||
f.close()
|
||||
|
||||
if self.flow_background == 'W':
|
||||
background_colour = 'white'
|
||||
if len(self.album_manager.model.store) <= self.flow_max:
|
||||
foreground_colour = 'white'
|
||||
else:
|
||||
foreground_colour = 'black'
|
||||
else:
|
||||
background_colour = 'black'
|
||||
if len(self.album_manager.model.store) <= self.flow_max:
|
||||
foreground_colour = 'black'
|
||||
else:
|
||||
foreground_colour = 'white'
|
||||
|
||||
string = string.replace('#BACKGROUND_COLOUR', background_colour)
|
||||
string = string.replace('#FOREGROUND_COLOUR', foreground_colour)
|
||||
string = string.replace('#FACTOR', str(float(self.flow_scale) / 100))
|
||||
|
||||
if self.flow_hide:
|
||||
caption = ""
|
||||
else:
|
||||
caption = '<div class="globalCaption"></div>'
|
||||
|
||||
string = string.replace('#GLOBAL_CAPTION', caption)
|
||||
|
||||
addon = background_colour
|
||||
if self.flow_appearance == 'flow-vert':
|
||||
addon += " vertical"
|
||||
elif self.flow_appearance == 'carousel':
|
||||
addon += " carousel"
|
||||
elif self.flow_appearance == 'roundabout':
|
||||
addon += " roundabout"
|
||||
|
||||
string = string.replace('#ADDON', addon)
|
||||
|
||||
string = string.replace('#WIDTH', str(self.flow_width))
|
||||
|
||||
identifier = self.flow.get_identifier(self.last_album)
|
||||
if not identifier:
|
||||
identifier = "'start'"
|
||||
else:
|
||||
identifier = str(identifier)
|
||||
|
||||
string = string.replace('#START', identifier)
|
||||
|
||||
#TRANSLATORS: for example 'Number of covers limited to 150'
|
||||
display_message = _("Number of covers limited to %d") % self.flow_max
|
||||
string = string.replace('#MAXCOVERS',
|
||||
'<p>' + display_message + '</p>')
|
||||
|
||||
items = self.flow.initialise(self.album_manager.model, self.flow_max)
|
||||
|
||||
string = string.replace('#ITEMS', items)
|
||||
|
||||
base = os.path.dirname(path) + "/"
|
||||
#Gdk.threads_enter()
|
||||
print(string)
|
||||
self.view.load_string(string, "text/html", "UTF-8", "file://" + base)
|
||||
#Gdk.threads_leave()
|
||||
|
||||
if self._on_first_use:
|
||||
self._on_first_use = False
|
||||
GLib.timeout_add(250, self.source.show_hide_pane, (self.last_album, PanedCollapsible.Paned.EXPAND))
|
||||
|
||||
def get_view_icon_name(self):
|
||||
return "flowview.png"
|
||||
|
||||
def initialise(self, source):
|
||||
if self._has_initialised:
|
||||
return
|
||||
|
||||
self._has_initialised = True
|
||||
|
||||
super(CoverFlowView, self).initialise(source)
|
||||
|
||||
self.album_manager = source.album_manager
|
||||
self.ext_menu_pos = 6
|
||||
|
||||
self._connect_properties()
|
||||
self._connect_signals(source)
|
||||
|
||||
# lets check that all covers have finished loading before
|
||||
# initialising the flowcontrol and other signals
|
||||
if not self.album_manager.cover_man.has_finished_loading:
|
||||
self.album_manager.cover_man.connect('load-finished', self._covers_loaded)
|
||||
else:
|
||||
self._covers_loaded()
|
||||
|
||||
def _covers_loaded(self, *args):
|
||||
self.flow = FlowControl(self)
|
||||
self.view.connect("notify::title", self.flow.receive_message_signal)
|
||||
|
||||
#self.album_manager.model.connect('album-updated', self.flow.update_album, self.view)
|
||||
#self.album_manager.model.connect('visual-updated', self.flow.update_album, self.view)
|
||||
self.album_manager.model.connect('album-updated', self.filter_changed)
|
||||
self.album_manager.model.connect('visual-updated', self.filter_changed)
|
||||
self.album_manager.model.connect('filter-changed', self.filter_changed)
|
||||
|
||||
self.filter_changed()
|
||||
|
||||
@property
|
||||
def last_album(self):
|
||||
return self._last_album
|
||||
|
||||
@last_album.setter
|
||||
def last_album(self, new_album):
|
||||
if self._last_album != new_album:
|
||||
self._last_album = new_album
|
||||
self.source.click_count = 0
|
||||
self.selectionchanged_callback()
|
||||
|
||||
def item_rightclicked_callback(self, album):
|
||||
self.last_album = album
|
||||
self.popup.popup(self.source, 'popup_menu', 3, Gtk.get_current_event_time())
|
||||
|
||||
def item_clicked_callback(self, album):
|
||||
'''
|
||||
Callback called when the user clicks somewhere on the flow_view.
|
||||
Along with source "show_hide_pane", takes care of showing/hiding the bottom
|
||||
pane after a second click on a selected album.
|
||||
'''
|
||||
# to expand the entry view
|
||||
if self.flow_automatic:
|
||||
self.source.click_count += 1
|
||||
|
||||
self.last_album = album
|
||||
|
||||
if self.source.click_count == 1:
|
||||
GLib.timeout_add(250, self.source.show_hide_pane, album)
|
||||
|
||||
def item_activated_callback(self, album):
|
||||
'''
|
||||
Callback called when the flow view is double clicked. It plays the selected album
|
||||
'''
|
||||
self.last_album = album
|
||||
self.source.play_selected_album()
|
||||
|
||||
return True
|
||||
|
||||
def item_drop_callback(self, album, webpath):
|
||||
'''
|
||||
Callback called when something is dropped onto the flow view - hopefully a webpath
|
||||
to a picture
|
||||
'''
|
||||
print("item_drop_callback %s" % webpath)
|
||||
print("dropped on album %s" % album)
|
||||
self.album_manager.cover_man.update_cover(album, uri=webpath)
|
||||
|
||||
def get_selected_objects(self):
|
||||
if self.last_album:
|
||||
return [self.last_album]
|
||||
else:
|
||||
return []
|
||||
|
||||
def select_and_scroll_to_path(self, path):
|
||||
album = self.source.album_manager.model.get_from_path(path)
|
||||
self.flow.scroll_to_album(album, self.view)
|
||||
self.item_clicked_callback(album)
|
||||
|
||||
def switch_to_view(self, source, album):
|
||||
self.initialise(source)
|
||||
self.show_policy.initialise(source.album_manager)
|
||||
|
||||
self.last_album = album
|
||||
self.scroll_to_album(self.last_album)
|
||||
|
||||
def grab_focus(self):
|
||||
self.view.grab_focus()
|
||||
|
||||
def scroll_to_album(self, album):
|
||||
self.flow.scroll_to_album(album, self.view)
|
||||
|
||||
|
||||
class FlowControl(object):
|
||||
def __init__(self, callback_view):
|
||||
self.callback_view = callback_view
|
||||
self.album_identifier = {}
|
||||
|
||||
def get_identifier(self, album):
|
||||
index = -1
|
||||
for row in self.album_identifier:
|
||||
if self.album_identifier[row] == album:
|
||||
index = row
|
||||
break
|
||||
|
||||
if index == -1:
|
||||
return None
|
||||
else:
|
||||
return row
|
||||
|
||||
def update_album(self, model, album_path, album_iter, webview):
|
||||
album = model.get_from_path(album_path)
|
||||
index = -1
|
||||
for row in self.album_identifier:
|
||||
if self.album_identifier[row] == album:
|
||||
index = row
|
||||
break
|
||||
|
||||
if index == -1:
|
||||
return
|
||||
|
||||
obj = {}
|
||||
obj['filename'] = album.cover.original
|
||||
obj['title'] = album.artist
|
||||
obj['caption'] = album.name
|
||||
obj['identifier'] = str(index)
|
||||
|
||||
webview.execute_script("update_album('%s')" % json.dumps(obj))
|
||||
|
||||
def receive_message_signal(self, webview, param):
|
||||
# this will be key to passing stuff back and forth - need
|
||||
# to develop some-sort of message protocol to distinguish "events"
|
||||
|
||||
title = webview.get_title()
|
||||
if (not title) or (title == '"clear"'):
|
||||
return
|
||||
|
||||
args = json.loads(title)
|
||||
try:
|
||||
signal = args["signal"]
|
||||
except:
|
||||
print("unhandled: %s " % title)
|
||||
return
|
||||
|
||||
if signal == 'clickactive':
|
||||
self.callback_view.item_clicked_callback(self.album_identifier[int(args['param'][0])])
|
||||
elif signal == 'rightclickactive':
|
||||
self.callback_view.item_rightclicked_callback(
|
||||
self.album_identifier[int(args['param'][0])])
|
||||
elif signal == 'doubleclickactive':
|
||||
self.callback_view.item_activated_callback(self.album_identifier[int(args['param'][0])])
|
||||
elif signal == 'dropactive':
|
||||
self.callback_view.item_drop_callback(self.album_identifier[int(args['param'][0])],
|
||||
args['param'][1])
|
||||
else:
|
||||
print("unhandled signal: %s" % signal)
|
||||
|
||||
def scroll_to_album(self, album, webview):
|
||||
for row in self.album_identifier:
|
||||
if self.album_identifier[row] == album:
|
||||
webview.execute_script("scroll_to_identifier('%s')" % str(row))
|
||||
break
|
||||
|
||||
def initialise(self, model, max_covers):
|
||||
|
||||
album_col = model.columns['album']
|
||||
index = 0
|
||||
items = ""
|
||||
self.album_identifier = {}
|
||||
|
||||
def html_elements(fullfilename, title, caption, identifier):
|
||||
|
||||
return '<div class="item"><img class="content" src="' + \
|
||||
escape(fullfilename) + '" title="' + \
|
||||
escape(title) + '" identifier="' + \
|
||||
identifier + '"/> <div class="caption">' + \
|
||||
escape(caption) + '</div> </div>'
|
||||
|
||||
|
||||
for row in model.store:
|
||||
|
||||
cover = row[album_col].cover.original
|
||||
cover = cover.replace(
|
||||
'rhythmbox-missing-artwork.svg',
|
||||
'rhythmbox-missing-artwork.png') # # need a white vs black when we change the background colour
|
||||
|
||||
self.album_identifier[index] = row[album_col]
|
||||
items += html_elements(
|
||||
fullfilename=cover,
|
||||
caption=row[album_col].name,
|
||||
title=row[album_col].artist,
|
||||
identifier=str(index))
|
||||
|
||||
index += 1
|
||||
|
||||
if index == max_covers:
|
||||
break
|
||||
|
||||
if index != 0:
|
||||
# self.callback_view.last_album = self.album_identifier[0]
|
||||
pass
|
||||
else:
|
||||
self.callback_view.last_album = None
|
||||
|
||||
return items
|
||||
@@ -0,0 +1,764 @@
|
||||
# -*- Mode: python; coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*-
|
||||
#
|
||||
# Copyright (C) 2012 - fossfreedom
|
||||
# Copyright (C) 2012 - Agustin Carrasco
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
import gettext
|
||||
|
||||
from gi.repository import Gdk
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import GLib
|
||||
from gi.repository import GObject
|
||||
from gi.repository import Gio
|
||||
from gi.repository import Pango
|
||||
from gi.repository import PangoCairo
|
||||
from gi.repository import GdkPixbuf
|
||||
|
||||
from coverart_widgets import EnhancedIconView
|
||||
from coverart_browser_prefs import GSetting
|
||||
from coverart_browser_prefs import CoverLocale
|
||||
from coverart_album import AlbumsModel
|
||||
from coverart_widgets import AbstractView
|
||||
from coverart_widgets import PanedCollapsible
|
||||
import rb
|
||||
|
||||
|
||||
PLAY_SIZE_X = 30
|
||||
PLAY_SIZE_Y = 30
|
||||
|
||||
|
||||
class CellRendererThumb(Gtk.CellRendererPixbuf):
|
||||
markup = GObject.property(type=str, default="")
|
||||
|
||||
def __init__(self, font_description, cell_area_source):
|
||||
super(CellRendererThumb, self).__init__()
|
||||
self.font_description = font_description
|
||||
self.cell_area_source = cell_area_source
|
||||
|
||||
def do_render(self, cr, widget,
|
||||
background_area,
|
||||
cell_area,
|
||||
flags):
|
||||
|
||||
x_offset = cell_area.x + 1
|
||||
y_offset = cell_area.y + 1
|
||||
# first paint the cover
|
||||
pixbuf = self.props.pixbuf.scale_simple(cell_area.width - 2, cell_area.height - 2,
|
||||
GdkPixbuf.InterpType.NEAREST)
|
||||
Gdk.cairo_set_source_pixbuf(cr, pixbuf, x_offset, y_offset)
|
||||
cr.paint()
|
||||
|
||||
alpha = 0.40
|
||||
|
||||
if ((flags & Gtk.CellRendererState.PRELIT) == Gtk.CellRendererState.PRELIT):
|
||||
# if the cursor is over the cell then slightly dim
|
||||
alpha -= 0.15
|
||||
|
||||
if self.cell_area_source.hover_pixbuf:
|
||||
# if a hover pixbuf is given then paint this as well either just above the cover album info
|
||||
# of at the bottom of the cell area if album info is not within the cover area
|
||||
full, calc_x_offset, calc_y_offset = self.cell_area_source.calc_play_icon_offset(x_offset, y_offset)
|
||||
|
||||
Gdk.cairo_set_source_pixbuf(cr,
|
||||
self.cell_area_source.hover_pixbuf,
|
||||
calc_x_offset,
|
||||
calc_y_offset - PLAY_SIZE_Y)
|
||||
cr.paint()
|
||||
|
||||
if not (self.cell_area_source.display_text and self.cell_area_source.display_text_pos == False):
|
||||
return
|
||||
|
||||
# the rest of the routine paints the contents of text within a cover if specified
|
||||
|
||||
# PANGO LAYOUT
|
||||
layout_width = cell_area.width - 2
|
||||
pango_layout = PangoCairo.create_layout(cr)
|
||||
pango_layout.set_markup(self.markup, -1)
|
||||
pango_layout.set_alignment(self.cell_area_source.text_alignment)
|
||||
pango_layout.set_font_description(self.font_description)
|
||||
pango_layout.set_width(int(layout_width * Pango.SCALE))
|
||||
pango_layout.set_wrap(Pango.WrapMode.WORD_CHAR)
|
||||
wi, he = pango_layout.get_pixel_size()
|
||||
|
||||
rect_offset = y_offset + (int((2.0 * self.cell_area_source.cover_size) / 3.0))
|
||||
rect_height = int(self.cell_area_source.cover_size / 3.0)
|
||||
|
||||
if he > rect_height:
|
||||
pango_layout.set_ellipsize(Pango.EllipsizeMode.END)
|
||||
pango_layout.set_height(int((self.cell_area_source.cover_size / 3.0) * Pango.SCALE))
|
||||
wi, he = pango_layout.get_pixel_size()
|
||||
|
||||
# RECTANGLE
|
||||
cr.set_source_rgba(0.0, 0.0, 0.0, alpha)
|
||||
cr.set_line_width(0)
|
||||
cr.rectangle(x_offset,
|
||||
rect_offset,
|
||||
cell_area.width - 1,
|
||||
rect_height - 1)
|
||||
cr.fill()
|
||||
|
||||
# DRAW FONT
|
||||
cr.set_source_rgba(1.0, 1.0, 1.0, 1.0)
|
||||
cr.move_to(x_offset,
|
||||
y_offset
|
||||
+ 2.0 * self.cell_area_source.cover_size / 3.0
|
||||
+ (((self.cell_area_source.cover_size / 3.0) - he) / 2.0)
|
||||
)
|
||||
PangoCairo.show_layout(cr, pango_layout)
|
||||
|
||||
|
||||
class AlbumArtCellArea(Gtk.CellAreaBox):
|
||||
font_family = GObject.property(type=str, default="Sans")
|
||||
font_size = GObject.property(type=int, default=10)
|
||||
cover_size = GObject.property(type=int, default=0)
|
||||
display_text_pos = GObject.property(type=bool, default=False)
|
||||
display_text = GObject.property(type=bool, default=False)
|
||||
add_shadow = GObject.property(type=bool, default=False)
|
||||
hover_pixbuf = GObject.property(type=object, default=None)
|
||||
text_alignment = GObject.property(type=int, default=1)
|
||||
|
||||
def __init__(self, ):
|
||||
super(AlbumArtCellArea, self).__init__()
|
||||
|
||||
self.font_description = Pango.FontDescription.new()
|
||||
self.font_description.set_family(self.font_family)
|
||||
self.font_description.set_size(int(self.font_size * Pango.SCALE))
|
||||
|
||||
self._connect_properties()
|
||||
|
||||
# Add own cellrenderer
|
||||
renderer_thumb = CellRendererThumb(self.font_description, self)
|
||||
|
||||
self.pack_start(renderer_thumb, False, False, False)
|
||||
self.attribute_connect(renderer_thumb, "pixbuf", AlbumsModel.columns['pixbuf'])
|
||||
self.attribute_connect(renderer_thumb, "markup", AlbumsModel.columns['markup'])
|
||||
self.props.spacing = 2
|
||||
|
||||
def _connect_properties(self):
|
||||
gs = GSetting()
|
||||
setting = gs.get_setting(gs.Path.PLUGIN)
|
||||
|
||||
setting.bind(gs.PluginKey.COVER_SIZE, self, 'cover-size',
|
||||
Gio.SettingsBindFlags.GET)
|
||||
|
||||
setting.bind(gs.PluginKey.DISPLAY_TEXT_POS, self, 'display-text-pos',
|
||||
Gio.SettingsBindFlags.GET)
|
||||
|
||||
setting.bind(gs.PluginKey.DISPLAY_TEXT, self, 'display-text',
|
||||
Gio.SettingsBindFlags.GET)
|
||||
|
||||
setting.bind(gs.PluginKey.ADD_SHADOW, self, 'add-shadow',
|
||||
Gio.SettingsBindFlags.GET)
|
||||
|
||||
setting.bind(gs.PluginKey.TEXT_ALIGNMENT, self, 'text-alignment',
|
||||
Gio.SettingsBindFlags.GET)
|
||||
|
||||
|
||||
def calc_play_icon_offset(self, initial_x_offset, initial_y_offset):
|
||||
'''
|
||||
calculates the x & y offset for the play hover icon
|
||||
:param initial_x_offset: current x_offset
|
||||
:param initial_y_offset: current y_offset
|
||||
:return: bool, x & y offset where bool is the full cover position
|
||||
'''
|
||||
full_cover = False
|
||||
if not (self.display_text and self.display_text_pos == False):
|
||||
y_offset = initial_y_offset + self.cover_size - 10
|
||||
full_cover = True
|
||||
else:
|
||||
y_offset = initial_y_offset + (int((2.0 * self.cover_size) / 3.0))
|
||||
|
||||
x_offset = initial_x_offset
|
||||
if self.add_shadow:
|
||||
x_offset = initial_x_offset + 10
|
||||
y_offset = y_offset - 10
|
||||
|
||||
return full_cover, x_offset, y_offset
|
||||
|
||||
|
||||
class AlbumShowingPolicy(GObject.Object):
|
||||
'''
|
||||
Policy that mostly takes care of how and when things should be showed on
|
||||
the view that makes use of the `AlbumsModel`.
|
||||
'''
|
||||
|
||||
def __init__(self, cover_view):
|
||||
super(AlbumShowingPolicy, self).__init__()
|
||||
|
||||
self._cover_view = cover_view # this will need to be reworked for all views
|
||||
self._visible_paths = None
|
||||
self._has_initialised = False
|
||||
|
||||
def initialise(self, album_manager):
|
||||
if self._has_initialised:
|
||||
return
|
||||
|
||||
self._album_manager = album_manager
|
||||
self._model = album_manager.model
|
||||
self._connect_signals()
|
||||
self._has_initialised = True
|
||||
|
||||
def _connect_signals(self):
|
||||
self._cover_view.props.vadjustment.connect('value-changed',
|
||||
self._viewport_changed)
|
||||
self._model.connect('album-updated', self._album_updated)
|
||||
self._model.connect('visual-updated', self._album_updated)
|
||||
|
||||
def _viewport_changed(self, *args):
|
||||
visible_range = self._cover_view.get_visible_range()
|
||||
|
||||
if visible_range:
|
||||
init, end = visible_range
|
||||
|
||||
# i have to use the tree iter instead of the path to iterate since
|
||||
# for some reason path.next doesn't work with the filtermodel
|
||||
tree_iter = self._model.store.get_iter(init)
|
||||
|
||||
self._visible_paths = []
|
||||
|
||||
while init and init != end:
|
||||
self._visible_paths.append(init)
|
||||
|
||||
tree_iter = self._model.store.iter_next(tree_iter)
|
||||
init = self._model.store.get_path(tree_iter)
|
||||
|
||||
self._visible_paths.append(end)
|
||||
|
||||
def _album_updated(self, model, album_path, album_iter):
|
||||
# get the currently showing paths
|
||||
if not self._visible_paths:
|
||||
self._viewport_changed()
|
||||
|
||||
if (album_path and self._visible_paths) and album_path in self._visible_paths:
|
||||
# if our path is on the viewport, emit the signal to update it
|
||||
self._cover_view.queue_draw()
|
||||
|
||||
|
||||
class CoverIconView(EnhancedIconView, AbstractView):
|
||||
__gtype_name__ = "CoverIconView"
|
||||
|
||||
icon_spacing = GObject.property(type=int, default=0)
|
||||
icon_padding = GObject.property(type=int, default=0)
|
||||
icon_automatic = GObject.property(type=bool, default=True)
|
||||
|
||||
display_text_enabled = GObject.property(type=bool, default=False)
|
||||
display_text_pos = GObject.property(type=bool, default=False)
|
||||
name = 'coverview'
|
||||
panedposition = PanedCollapsible.Paned.COLLAPSE
|
||||
text_alignment = GObject.property(type=int, default=1)
|
||||
|
||||
__gsignals__ = {
|
||||
'update-toolbar': (GObject.SIGNAL_RUN_LAST, None, ())
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CoverIconView, self).__init__(cell_area=AlbumArtCellArea(), *args, **kwargs)
|
||||
|
||||
self.gs = GSetting()
|
||||
# custom text renderer
|
||||
self._text_renderer = None
|
||||
self.show_policy = AlbumShowingPolicy(self)
|
||||
self.view = self
|
||||
self._has_initialised = False
|
||||
self._last_path = None
|
||||
self._calc_motion_step = 0
|
||||
self.set_selection_mode(Gtk.SelectionMode.MULTIPLE)
|
||||
self.object_column = AlbumsModel.columns['album']
|
||||
|
||||
def initialise(self, source):
|
||||
if self._has_initialised:
|
||||
return
|
||||
|
||||
self._has_initialised = True
|
||||
|
||||
self.view_name = "covers_view"
|
||||
super(CoverIconView, self).initialise(source)
|
||||
|
||||
self.shell = source.shell
|
||||
self.album_manager = source.album_manager
|
||||
|
||||
# setup iconview drag&drop support
|
||||
# first drag and drop on the coverart view to receive coverart
|
||||
self.enable_model_drag_dest([], Gdk.DragAction.COPY)
|
||||
self.drag_dest_add_image_targets()
|
||||
self.drag_dest_add_text_targets()
|
||||
self.connect('drag-drop', self.on_drag_drop)
|
||||
self.connect('drag-data-received',
|
||||
self.on_drag_data_received)
|
||||
self.source.paned.connect("expanded", self.bottom_expander_expanded_callback)
|
||||
|
||||
# lastly support drag-drop from coverart to devices/nautilus etc
|
||||
self.connect('drag-begin', self.on_drag_begin)
|
||||
self.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK,
|
||||
[], Gdk.DragAction.COPY)
|
||||
# targets = Gtk.TargetList.new([Gtk.TargetEntry.new("application/x-rhythmbox-entry", 0, 0),
|
||||
# Gtk.TargetEntry.new("text/uri-list", 0, 1) ])
|
||||
targets = Gtk.TargetList.new([Gtk.TargetEntry.new("text/uri-list", 0, 0)])
|
||||
# N.B. values taken from rhythmbox v2.97 widgets/rb_entry_view.c
|
||||
targets.add_uri_targets(1)
|
||||
|
||||
self.drag_source_set_target_list(targets)
|
||||
self.connect("drag-data-get", self.on_drag_data_get)
|
||||
|
||||
# set the model to the view
|
||||
# self.set_pixbuf_column(AlbumsModel.columns['pixbuf'])
|
||||
self.set_model(self.album_manager.model.store)
|
||||
|
||||
# setup view to monitor mouse movements
|
||||
self.add_events(Gdk.EventMask.POINTER_MOTION_MASK)
|
||||
|
||||
self.hover_pixbufs = {
|
||||
'button_play': None,
|
||||
'button_play_hover': None,
|
||||
'button_playpause': None,
|
||||
'button_playpause_hover': None,
|
||||
'button_queue': None,
|
||||
'button_queue_hover': None,
|
||||
}
|
||||
|
||||
for pixbuf_type in self.hover_pixbufs:
|
||||
filename = 'img/' + pixbuf_type + '.png'
|
||||
filename = rb.find_plugin_file(self.plugin, filename)
|
||||
self.hover_pixbufs[pixbuf_type] = GdkPixbuf.Pixbuf.new_from_file_at_size(filename,
|
||||
PLAY_SIZE_X,
|
||||
PLAY_SIZE_Y)
|
||||
|
||||
self._connect_properties()
|
||||
self._connect_signals()
|
||||
|
||||
self._activate_markup()
|
||||
self.on_notify_icon_padding()
|
||||
self.on_notify_icon_spacing()
|
||||
|
||||
def _connect_properties(self):
|
||||
setting = self.gs.get_setting(self.gs.Path.PLUGIN)
|
||||
setting.bind(
|
||||
self.gs.PluginKey.ICON_SPACING,
|
||||
self,
|
||||
'icon_spacing',
|
||||
Gio.SettingsBindFlags.GET)
|
||||
setting.bind(
|
||||
self.gs.PluginKey.ICON_PADDING,
|
||||
self,
|
||||
'icon_padding',
|
||||
Gio.SettingsBindFlags.GET)
|
||||
|
||||
setting.bind(self.gs.PluginKey.DISPLAY_TEXT, self,
|
||||
'display_text_enabled', Gio.SettingsBindFlags.GET)
|
||||
|
||||
setting.bind(self.gs.PluginKey.ICON_AUTOMATIC, self,
|
||||
'icon_automatic', Gio.SettingsBindFlags.GET)
|
||||
|
||||
setting.bind(self.gs.PluginKey.DISPLAY_TEXT_POS, self,
|
||||
'display-text-pos', Gio.SettingsBindFlags.GET)
|
||||
|
||||
setting.bind(self.gs.PluginKey.TEXT_ALIGNMENT, self,
|
||||
'text-alignment', Gio.SettingsBindFlags.GET)
|
||||
|
||||
def _connect_signals(self):
|
||||
self.connect("item-clicked", self.item_clicked_callback)
|
||||
self.connect("selection-changed", self.selectionchanged_callback)
|
||||
self.connect("item-activated", self.item_activated_callback)
|
||||
self.connect('notify::icon-spacing',
|
||||
self.on_notify_icon_spacing)
|
||||
self.connect('notify::icon-padding',
|
||||
self.on_notify_icon_padding)
|
||||
self.connect('notify::display-text-enabled',
|
||||
self._activate_markup)
|
||||
self.connect('notify::display-text-pos',
|
||||
self._activate_markup)
|
||||
self.connect('notify::text-alignment',
|
||||
self._create_and_configure_renderer)
|
||||
self.connect("motion-notify-event", self.on_pointer_motion)
|
||||
|
||||
self.add_events(Gdk.EventMask.SCROLL_MASK)
|
||||
self.connect("scroll-event", self.on_scroll_event)
|
||||
|
||||
def get_view_icon_name(self):
|
||||
return "iconview.png"
|
||||
|
||||
def resize_icon(self, cover_size):
|
||||
'''
|
||||
Callback called when to resize the icon
|
||||
[common to all views]
|
||||
'''
|
||||
self.set_item_width(cover_size)
|
||||
|
||||
def on_scroll_event(self, widget, scroll_event):
|
||||
if scroll_event.state & Gdk.ModifierType.CONTROL_MASK:
|
||||
settings = self.gs.get_setting(self.gs.Path.PLUGIN)
|
||||
cover_size = settings[self.gs.PluginKey.COVER_SIZE]
|
||||
if scroll_event.direction == Gdk.ScrollDirection.UP:
|
||||
if cover_size <= 195:
|
||||
settings[self.gs.PluginKey.COVER_SIZE] = cover_size + 5
|
||||
elif scroll_event.direction == Gdk.ScrollDirection.DOWN:
|
||||
if cover_size >= 55:
|
||||
settings[self.gs.PluginKey.COVER_SIZE] = cover_size - 5
|
||||
elif scroll_event.direction == Gdk.ScrollDirection.SMOOTH:
|
||||
delta = scroll_event.delta_y
|
||||
print (delta)
|
||||
if delta < 0 and cover_size <= 195: # negative delta means scroll up
|
||||
settings[self.gs.PluginKey.COVER_SIZE] = cover_size - int(delta * 5)
|
||||
if delta > 0 and cover_size >= 55: # positive delta means scroll down
|
||||
settings[self.gs.PluginKey.COVER_SIZE] = cover_size - int(delta * 5)
|
||||
|
||||
GLib.idle_add(self.queue_draw)
|
||||
|
||||
return True
|
||||
|
||||
def on_drag_drop(self, widget, context, x, y, time):
|
||||
'''
|
||||
Callback called when a drag operation finishes over the cover view
|
||||
of the source. It decides if the dropped item can be processed as
|
||||
an image to use as a cover.
|
||||
'''
|
||||
|
||||
# stop the propagation of the signal (deactivates superclass callback)
|
||||
widget.stop_emission_by_name('drag-drop')
|
||||
|
||||
# obtain the path of the icon over which the drag operation finished
|
||||
path, pos = widget.get_dest_item_at_pos(x, y)
|
||||
result = path is not None
|
||||
|
||||
if result:
|
||||
target = self.drag_dest_find_target(context, None)
|
||||
widget.drag_get_data(context, target, time)
|
||||
|
||||
return result
|
||||
|
||||
def on_drag_data_received(self, widget, drag_context, x, y, data, info,
|
||||
time):
|
||||
'''
|
||||
Callback called when the drag source has prepared the data (pixbuf)
|
||||
for us to use.
|
||||
'''
|
||||
|
||||
# stop the propagation of the signal (deactivates superclass callback)
|
||||
widget.stop_emission_by_name('drag-data-received')
|
||||
|
||||
# get the album and the info and ask the loader to update the cover
|
||||
path, pos = widget.get_dest_item_at_pos(x, y)
|
||||
album = widget.get_model()[path][2]
|
||||
|
||||
pixbuf = data.get_pixbuf()
|
||||
|
||||
if pixbuf:
|
||||
self.album_manager.cover_man.update_cover(album, pixbuf)
|
||||
else:
|
||||
uri = data.get_text()
|
||||
self.album_manager.cover_man.update_cover(album, uri=uri)
|
||||
|
||||
# call the context drag_finished to inform the source about it
|
||||
drag_context.finish(True, False, time)
|
||||
|
||||
|
||||
def on_drag_data_get(self, widget, drag_context, data, info, time):
|
||||
'''
|
||||
Callback called when the drag destination (playlist) has
|
||||
requested what album (icon) has been dragged
|
||||
'''
|
||||
|
||||
uris = []
|
||||
for album in widget.get_selected_objects():
|
||||
for track in album.get_tracks():
|
||||
uris.append(track.location)
|
||||
|
||||
sel = data.set_uris(uris)
|
||||
# stop the propagation of the signal (deactivates superclass callback)
|
||||
widget.stop_emission_by_name('drag-data-get')
|
||||
|
||||
def on_drag_begin(self, widget, context):
|
||||
'''
|
||||
Callback called when the drag-drop from coverview has started
|
||||
Changes the drag icon as appropriate
|
||||
'''
|
||||
album_number = len(widget.get_selected_objects())
|
||||
|
||||
if album_number == 1:
|
||||
item = Gtk.STOCK_DND
|
||||
else:
|
||||
item = Gtk.STOCK_DND_MULTIPLE
|
||||
|
||||
widget.drag_source_set_icon_stock(item)
|
||||
widget.stop_emission_by_name('drag-begin')
|
||||
|
||||
def _cover_play_hotspot(self, path, in_vacinity=False):
|
||||
if path:
|
||||
valid, rect = self.get_cell_rect(path, None) # rect of widget coords
|
||||
|
||||
cursor_x, cursor_y = self.get_pointer() # returns widget coords
|
||||
c_x = cursor_x - rect.x - (self.icon_padding / 2) - (self.icon_spacing / 2)
|
||||
c_y = cursor_y - rect.y - (self.icon_padding / 2) - (self.icon_spacing / 2)
|
||||
|
||||
sizing = (rect.width / 3) if in_vacinity else 0
|
||||
|
||||
full, x_offset, y_offset = self.props.cell_area.calc_play_icon_offset(0, 0)
|
||||
if full and c_y > y_offset:
|
||||
return False
|
||||
|
||||
y_offset = y_offset - PLAY_SIZE_Y
|
||||
|
||||
if (y_offset - PLAY_SIZE_Y) < 0:
|
||||
return False
|
||||
|
||||
if c_x < (PLAY_SIZE_X + sizing + x_offset) and \
|
||||
c_y < (PLAY_SIZE_Y + sizing + y_offset) and \
|
||||
c_x > x_offset and \
|
||||
c_y > (y_offset - sizing):
|
||||
return True
|
||||
|
||||
# c_y 0 value at top - largest at bottom of the cover
|
||||
return False
|
||||
|
||||
def on_pointer_motion(self, widget, event):
|
||||
self._current_mouse_x = event.x
|
||||
self._current_mouse_y = event.y
|
||||
|
||||
if self._calc_motion_step == 0:
|
||||
self._calc_motion_step = 1
|
||||
Gdk.threads_add_timeout(GLib.PRIORITY_DEFAULT_IDLE, 100,
|
||||
self._calculate_hotspot)
|
||||
else:
|
||||
path = self.get_path_at_pos(self._current_mouse_x,
|
||||
self._current_mouse_y)
|
||||
|
||||
if not self._last_path or self._last_path != path:
|
||||
self._display_icon(None, self._last_path)
|
||||
|
||||
def _display_icon(self, icon, path):
|
||||
self.props.cell_area.hover_pixbuf = icon
|
||||
if path and self.props.window:
|
||||
valid, rect = self.get_cell_rect(path, None)
|
||||
self.props.window.invalidate_rect(rect, True)
|
||||
|
||||
self.queue_draw()
|
||||
|
||||
def _calculate_hotspot(self, *args):
|
||||
|
||||
path = self.get_path_at_pos(self._current_mouse_x,
|
||||
self._current_mouse_y)
|
||||
|
||||
# if the current path was not the same as the last path then
|
||||
# reset the counter
|
||||
if not self._last_path or self._last_path != path:
|
||||
self._display_icon(None, self._last_path)
|
||||
self._last_path = path
|
||||
self._calc_motion_step = 0
|
||||
return False
|
||||
|
||||
self._calc_motion_step = self._calc_motion_step + 1
|
||||
|
||||
# if havent yet reached the requisite number of steps then
|
||||
# let the thread roll to the next increment
|
||||
if self._calc_motion_step < 8:
|
||||
return True
|
||||
|
||||
if not self._cover_play_hotspot(path, in_vacinity=True):
|
||||
# we are not near the hot-spot so decrement the counter
|
||||
# hoping next time around we are near
|
||||
self._calc_motion_step = self._calc_motion_step - 1
|
||||
self._display_icon(None, self._last_path)
|
||||
return True
|
||||
|
||||
# from here on in, we are going to display a hotspot icon
|
||||
# so lets decide which one
|
||||
|
||||
(_, playing) = self.shell.props.shell_player.get_playing()
|
||||
|
||||
calc_path = -1
|
||||
if playing:
|
||||
entry = self.shell.props.shell_player.get_playing_entry()
|
||||
album = self.album_manager.model.get_from_dbentry(entry)
|
||||
calc_path = self.album_manager.model.get_path(album)
|
||||
|
||||
if playing and calc_path == path:
|
||||
icon = 'button_playpause'
|
||||
elif playing:
|
||||
icon = 'button_queue'
|
||||
else:
|
||||
icon = 'button_play'
|
||||
|
||||
# now we've got the icon - lets double check that we are
|
||||
# actually hovering exactly on the hotspot because the icon will visually change
|
||||
|
||||
exact_hotspot = self._cover_play_hotspot(path)
|
||||
if exact_hotspot:
|
||||
icon = icon + '_hover'
|
||||
|
||||
hover = self.hover_pixbufs[icon]
|
||||
|
||||
self._display_icon(hover, path)
|
||||
self._calc_motion_step = self._calc_motion_step - 1
|
||||
|
||||
return True
|
||||
|
||||
def item_clicked_callback(self, iconview, event, path):
|
||||
'''
|
||||
Callback called when the user clicks somewhere on the cover_view.
|
||||
Along with source "show_hide_pane", takes care of showing/hiding the bottom
|
||||
pane after a second click on a selected album.
|
||||
'''
|
||||
|
||||
# first test if we've clicked on the cover-play icon
|
||||
if self._cover_play_hotspot(path):
|
||||
(_, playing) = self.shell.props.shell_player.get_playing()
|
||||
|
||||
# first see if anything is playing...
|
||||
if playing:
|
||||
entry = self.shell.props.shell_player.get_playing_entry()
|
||||
album = self.album_manager.model.get_from_dbentry(entry)
|
||||
|
||||
# if the current playing entry corresponds to the album
|
||||
# we are hovering over then we are requesting to pause
|
||||
if self.album_manager.model.get_from_path(path) == album:
|
||||
self._last_path = path
|
||||
self.shell.props.shell_player.pause()
|
||||
self.on_pointer_motion(self, event)
|
||||
return
|
||||
|
||||
# this must be a new album so we are asking just
|
||||
# to play this new album ... just need a short interval
|
||||
# for the selection event to kick in first
|
||||
def delay(*args):
|
||||
if playing: # if we are playing then queue up the next album
|
||||
self.source.queue_selected_album(None, self.source.favourites)
|
||||
album = self.get_selected_objects()[0]
|
||||
cl = CoverLocale()
|
||||
cl.switch_locale(cl.Locale.LOCALE_DOMAIN)
|
||||
message = gettext.gettext('Album has added to list of playing albums')
|
||||
self.display_notification(album.name,
|
||||
message,
|
||||
album.cover.original)
|
||||
else: # otherwise just play it
|
||||
self._last_path = path
|
||||
self.source.play_selected_album(self.source.favourites)
|
||||
|
||||
icon = 'button_play_hover'
|
||||
self.props.cell_area.hover_pixbuf = \
|
||||
self.hover_pixbufs[icon]
|
||||
|
||||
Gdk.threads_add_timeout(GLib.PRIORITY_DEFAULT_IDLE, 250,
|
||||
delay, None)
|
||||
|
||||
return
|
||||
|
||||
# to expand the entry view
|
||||
ctrl = event.state & Gdk.ModifierType.CONTROL_MASK
|
||||
shift = event.state & Gdk.ModifierType.SHIFT_MASK
|
||||
|
||||
if self.icon_automatic:
|
||||
self.source.click_count += 1 if not ctrl and not shift else 0
|
||||
|
||||
if self.source.click_count == 1:
|
||||
album = self.album_manager.model.get_from_path(path) \
|
||||
if path else None
|
||||
Gdk.threads_add_timeout(GLib.PRIORITY_DEFAULT_IDLE, 250,
|
||||
self.source.show_hide_pane, album)
|
||||
|
||||
def item_activated_callback(self, iconview, path):
|
||||
'''
|
||||
Callback called when the cover view is double clicked or space-bar
|
||||
is pressed. It plays the selected album
|
||||
'''
|
||||
self.source.play_selected_album(self.source.favourites)
|
||||
|
||||
return True
|
||||
|
||||
def on_notify_icon_padding(self, *args):
|
||||
'''
|
||||
Callback called when the icon-padding gsetting value is changed
|
||||
'''
|
||||
self.set_item_padding(self.icon_padding)
|
||||
|
||||
def on_notify_icon_spacing(self, *args):
|
||||
'''
|
||||
Callback called when the icon-spacing gsetting value is changed
|
||||
'''
|
||||
self.set_row_spacing(self.icon_spacing)
|
||||
self.set_column_spacing(self.icon_spacing)
|
||||
|
||||
def _create_and_configure_renderer(self, *args):
|
||||
if not self._text_renderer:
|
||||
# Add own cellrenderer
|
||||
self._text_renderer = Gtk.CellRendererText()
|
||||
|
||||
self._text_renderer.props.alignment = self.text_alignment
|
||||
self._text_renderer.props.wrap_mode = Pango.WrapMode.WORD
|
||||
if self.text_alignment == 1:
|
||||
self._text_renderer.props.xalign = 0.5
|
||||
elif self.text_alignment == 0:
|
||||
self._text_renderer.props.xalign = 0
|
||||
else:
|
||||
self._text_renderer.props.xalign = 1
|
||||
|
||||
self._text_renderer.props.yalign = 0
|
||||
self._text_renderer.props.width = \
|
||||
self.album_manager.cover_man.cover_size
|
||||
self._text_renderer.props.wrap_width = \
|
||||
self.album_manager.cover_man.cover_size
|
||||
|
||||
def _activate_markup(self, *args):
|
||||
'''
|
||||
Utility method to activate/deactivate the markup text on the
|
||||
cover view.
|
||||
'''
|
||||
if self.display_text_enabled and self.display_text_pos:
|
||||
if not self._text_renderer:
|
||||
# create and configure the custom cell renderer
|
||||
self._create_and_configure_renderer()
|
||||
|
||||
# set the renderer
|
||||
self.pack_end(self._text_renderer, False)
|
||||
self.add_attribute(self._text_renderer,
|
||||
'markup', AlbumsModel.columns['markup'])
|
||||
elif self._text_renderer:
|
||||
# remove the cell renderer
|
||||
self.props.cell_area.remove(self._text_renderer)
|
||||
|
||||
if self.display_text_enabled:
|
||||
self.set_tooltip_column(-1) # turnoff tooltips
|
||||
else:
|
||||
self.set_tooltip_column(AlbumsModel.columns['tooltip'])
|
||||
|
||||
def bottom_expander_expanded_callback(self, paned, expand):
|
||||
'''
|
||||
Callback connected to expanded signal of the paned GtkExpander
|
||||
'''
|
||||
if expand:
|
||||
# accommodate the viewport if there's an album selected
|
||||
if self.source.last_selected_album:
|
||||
def scroll_to_album(*args):
|
||||
# accommodate the viewport if there's an album selected
|
||||
path = self.album_manager.model.get_path(
|
||||
self.source.last_selected_album)
|
||||
|
||||
self.scroll_to_path(path, False, 0, 0)
|
||||
|
||||
return False
|
||||
|
||||
Gdk.threads_add_idle(GObject.PRIORITY_DEFAULT_IDLE,
|
||||
scroll_to_album, None)
|
||||
|
||||
|
||||
def switch_to_view(self, source, album):
|
||||
self.initialise(source)
|
||||
self.show_policy.initialise(source.album_manager)
|
||||
|
||||
self.scroll_to_album(album)
|
||||
|
||||
def grab_focus(self):
|
||||
super(EnhancedIconView, self).grab_focus()
|
||||
@@ -0,0 +1,363 @@
|
||||
# -*- Mode: python; coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*-
|
||||
#
|
||||
# Copyright (C) 2012 - fossfreedom
|
||||
# Copyright (C) 2012 - Agustin Carrasco
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of thie GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
import shutil
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
|
||||
from gi.repository import GObject
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import GLib
|
||||
from gi.repository import RB
|
||||
from gi.repository import Gdk
|
||||
from gi.repository import Peas
|
||||
from gi.repository import Gst
|
||||
|
||||
from coverart_utils import NaturalString
|
||||
import rb
|
||||
import coverart_rb3compat as rb3compat
|
||||
|
||||
|
||||
class CoverArtExport(GObject.Object):
|
||||
'''
|
||||
This class provides for various export routines
|
||||
|
||||
'''
|
||||
TARGET_BITRATE = 128
|
||||
|
||||
def __init__(self, plugin, shell, album_manager):
|
||||
self.plugin = plugin
|
||||
self.shell = shell
|
||||
self.album_manager = album_manager
|
||||
|
||||
self._gstreamer_has_initialised = False
|
||||
self.has_opened_previously = False
|
||||
self._values = {}
|
||||
|
||||
def is_search_plugin_enabled(self):
|
||||
peas = Peas.Engine.get_default()
|
||||
loaded_plugins = peas.get_loaded_plugins()
|
||||
|
||||
result = False
|
||||
if 'coverart_search_providers' in loaded_plugins:
|
||||
info = peas.get_plugin_info('coverart_search_providers')
|
||||
version = info.get_version()
|
||||
|
||||
if NaturalString(version) >= "0.9":
|
||||
result = True
|
||||
|
||||
return result
|
||||
|
||||
def embed_albums(self, selected_albums):
|
||||
'''
|
||||
method to export and embed coverart to chosen albums
|
||||
|
||||
:selected_albums: `Album` - array of albums
|
||||
'''
|
||||
|
||||
self._initialise_gstreamer()
|
||||
|
||||
from coverart_search_tracks import CoverArtTracks
|
||||
|
||||
search_tracks = CoverArtTracks()
|
||||
playlist_manager = self.shell.props.playlist_manager
|
||||
playlists_entries = playlist_manager.get_playlists()
|
||||
|
||||
ui = Gtk.Builder()
|
||||
ui.add_from_file(rb.find_plugin_file(self.plugin,
|
||||
'ui/coverart_exportembed.ui'))
|
||||
ui.connect_signals(self)
|
||||
embeddialog = ui.get_object('exportembeddialog')
|
||||
embeddialog.set_transient_for(self.shell.props.window)
|
||||
folderchooserbutton = ui.get_object('folderchooserbutton')
|
||||
use_album_name_checkbutton = ui.get_object('use_album_name_checkbutton')
|
||||
open_filemanager_checkbutton = ui.get_object('open_filemanager_checkbutton')
|
||||
convert_checkbutton = ui.get_object('convert_checkbutton')
|
||||
bitrate_spinbutton = ui.get_object('bitrate_spinbutton')
|
||||
resize_checkbutton = ui.get_object('resize_checkbutton')
|
||||
resize_spinbutton = ui.get_object('resize_spinbutton')
|
||||
|
||||
# predefine values if not previously opened the dialog
|
||||
if self.has_opened_previously:
|
||||
print (self._values)
|
||||
if not self._values['toresize']:
|
||||
resize_spinbutton.set_value(128)
|
||||
else:
|
||||
resize_spinbutton.set_value(self._values['resize'])
|
||||
|
||||
if not self._values['convert']:
|
||||
bitrate_spinbutton.set_value(self.TARGET_BITRATE)
|
||||
else:
|
||||
bitrate_spinbutton.set_value(self._values['bitrate'])
|
||||
|
||||
folderchooserbutton.set_current_folder(self._values['final_folder_store'])
|
||||
use_album_name_checkbutton.set_active(self._values['use_album_name'])
|
||||
open_filemanager_checkbutton.set_active(self._values['open_filemanager'])
|
||||
convert_checkbutton.set_active(self._values['convert'])
|
||||
resize_checkbutton.set_active(self._values['toresize'])
|
||||
|
||||
else:
|
||||
bitrate_spinbutton.set_value(self.TARGET_BITRATE)
|
||||
resize_spinbutton.set_value(128)
|
||||
|
||||
downloads_dir = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_DOWNLOAD)
|
||||
folderchooserbutton.set_current_folder(downloads_dir)
|
||||
|
||||
response = embeddialog.run()
|
||||
|
||||
if response != Gtk.ResponseType.OK:
|
||||
embeddialog.destroy()
|
||||
return
|
||||
|
||||
self.has_opened_previously = True
|
||||
# ok pressed - now fetch values from the dialog
|
||||
final_folder_store = folderchooserbutton.get_current_folder()
|
||||
use_album_name = use_album_name_checkbutton.get_active()
|
||||
open_filemanager = open_filemanager_checkbutton.get_active()
|
||||
convert = convert_checkbutton.get_active()
|
||||
bitrate = bitrate_spinbutton.get_value()
|
||||
toresize = resize_checkbutton.get_active()
|
||||
if toresize:
|
||||
resize = int(resize_spinbutton.get_value())
|
||||
else:
|
||||
resize = -1
|
||||
|
||||
self._values['bitrate'] = bitrate
|
||||
self._values['resize'] = resize
|
||||
self._values['final_folder_store'] = final_folder_store
|
||||
self._values['use_album_name'] = use_album_name
|
||||
self._values['open_filemanager'] = open_filemanager
|
||||
self._values['convert'] = convert
|
||||
self._values['toresize'] = toresize
|
||||
|
||||
print (self._values)
|
||||
embeddialog.destroy()
|
||||
|
||||
albums = {}
|
||||
total = 0
|
||||
|
||||
for album in selected_albums:
|
||||
albums[album] = album.get_tracks()
|
||||
total = total + len(albums[album])
|
||||
|
||||
self._track_count = 1
|
||||
|
||||
def complete():
|
||||
self.album_manager.progress = 1
|
||||
|
||||
if open_filemanager:
|
||||
#code taken from http://stackoverflow.com/questions/1795111/is-there-a-cross-platform-way-to-open-a-file-browser-in-python
|
||||
if sys.platform == 'win32':
|
||||
import winreg
|
||||
|
||||
path = r('SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon')
|
||||
for root in (winreg.HKEY_CURRENT_USER, winreg.HKEY_LOCAL_MACHINE):
|
||||
try:
|
||||
with winreg.OpenKey(root, path) as k:
|
||||
value, regtype = winreg.QueryValueEx(k, 'Shell')
|
||||
except WindowsError:
|
||||
pass
|
||||
else:
|
||||
if regtype in (winreg.REG_SZ, winreg.REG_EXPAND_SZ):
|
||||
shell = value
|
||||
break
|
||||
else:
|
||||
shell = 'Explorer.exe'
|
||||
subprocess.Popen([shell, final_folder_store])
|
||||
|
||||
elif sys.platform == 'darwin':
|
||||
subprocess.Popen(['open', final_folder_store])
|
||||
|
||||
else:
|
||||
subprocess.Popen(['xdg-open', final_folder_store])
|
||||
|
||||
self._albumiter = iter(albums)
|
||||
self._tracknumber = 0
|
||||
self._album = next(self._albumiter)
|
||||
|
||||
def idle_call(data):
|
||||
exit_idle = True
|
||||
|
||||
track = albums[self._album][self._tracknumber]
|
||||
|
||||
if not process_track(self._album, track):
|
||||
exit_idle = False
|
||||
|
||||
self._tracknumber = self._tracknumber + 1
|
||||
|
||||
if self._tracknumber >= len(albums[self._album]):
|
||||
try:
|
||||
self._tracknumber = 0
|
||||
self._album = next(self._albumiter)
|
||||
except StopIteration:
|
||||
exit_idle = False
|
||||
|
||||
if not exit_idle:
|
||||
complete()
|
||||
|
||||
return exit_idle
|
||||
|
||||
def process_track(album, track):
|
||||
self.album_manager.progress = self._track_count / total
|
||||
self._track_count = self._track_count + 1
|
||||
|
||||
key = album.create_ext_db_key()
|
||||
finalPath = rb3compat.unquote(track.location)[7:]
|
||||
album_name = RB.search_fold(album.name)
|
||||
|
||||
if use_album_name:
|
||||
folder_store = final_folder_store + '/' + album_name
|
||||
else:
|
||||
folder_store = final_folder_store
|
||||
|
||||
try:
|
||||
if not os.path.exists(folder_store):
|
||||
os.makedirs(folder_store)
|
||||
|
||||
if convert:
|
||||
self.convert_to_mp3(finalPath, folder_store, bitrate)
|
||||
finalPath = self._calc_mp3_filename(finalPath, folder_store)
|
||||
print(finalPath)
|
||||
else:
|
||||
shutil.copy(finalPath, folder_store)
|
||||
except IOError as err:
|
||||
print(err.args[0])
|
||||
return False
|
||||
|
||||
dest = os.path.join(folder_store, os.path.basename(finalPath))
|
||||
desturi = 'file://' + rb3compat.pathname2url(dest)
|
||||
|
||||
return search_tracks.embed(desturi, key, resize)
|
||||
|
||||
data = None
|
||||
|
||||
Gdk.threads_add_idle(GLib.PRIORITY_DEFAULT_IDLE, idle_call, data)
|
||||
|
||||
def _initialise_gstreamer(self):
|
||||
|
||||
if self._gstreamer_has_initialised:
|
||||
return
|
||||
|
||||
self._gstreamer_has_initialised = True
|
||||
Gst.init(None)
|
||||
|
||||
def on_new_decoded_pad(dbin, pad):
|
||||
decode = pad.get_parent()
|
||||
pipeline = decode.get_parent()
|
||||
convert = pipeline.get_by_name('convert')
|
||||
decode.link(convert)
|
||||
|
||||
# we are going to mimic the following
|
||||
# gst-launch-1.0 filesrc location="02 - ABBA - Knowing Me, Knowing You.ogg" !
|
||||
# decodebin ! audioconvert ! audioresample ! lamemp3enc target=bitrate bitrate=128 !
|
||||
# xingmux ! id3v2mux ! filesink location="mytrack.mp3"
|
||||
|
||||
converter = Gst.Pipeline.new('converter')
|
||||
|
||||
source = Gst.ElementFactory.make('filesrc', None)
|
||||
|
||||
decoder = Gst.ElementFactory.make('decodebin', 'decoder')
|
||||
convert = Gst.ElementFactory.make('audioconvert', 'convert')
|
||||
sample = Gst.ElementFactory.make('audioresample', 'sample')
|
||||
encoder = Gst.ElementFactory.make('lamemp3enc', 'encoder')
|
||||
encoder.set_property('target', 'bitrate')
|
||||
encoder.set_property('bitrate', self.TARGET_BITRATE)
|
||||
|
||||
xing = Gst.ElementFactory.make('xingmux', 'xing') # needed to make bitrate more accurate
|
||||
mux = Gst.ElementFactory.make('id3v2mux', 'mux')
|
||||
if not mux:
|
||||
# use id3mux where not available
|
||||
mux = Gst.ElementFactory.make('id3mux', 'mux')
|
||||
|
||||
sink = Gst.ElementFactory.make('filesink', 'sink')
|
||||
|
||||
converter.add(source)
|
||||
converter.add(decoder)
|
||||
converter.add(convert)
|
||||
converter.add(sample)
|
||||
converter.add(encoder)
|
||||
converter.add(xing)
|
||||
converter.add(mux)
|
||||
converter.add(sink)
|
||||
|
||||
Gst.Element.link(source, decoder)
|
||||
#note - a decodebin cannot be linked at compile since
|
||||
#it doesnt have source-pads (http://stackoverflow.com/questions/2993777/gstreamer-of-pythons-gst-linkerror-problem)
|
||||
|
||||
decoder.connect("pad-added", on_new_decoded_pad)
|
||||
|
||||
Gst.Element.link(convert, sample)
|
||||
Gst.Element.link(sample, encoder)
|
||||
Gst.Element.link(encoder, xing)
|
||||
Gst.Element.link(xing, mux)
|
||||
Gst.Element.link(mux, sink)
|
||||
|
||||
self.converter = converter
|
||||
self.source = source
|
||||
self.sink = sink
|
||||
self.encoder = encoder
|
||||
|
||||
def _calc_mp3_filename(self, filename, save_folder):
|
||||
finalname = os.path.basename(filename)
|
||||
finalname = finalname.rsplit('.')[0] + ".mp3"
|
||||
return save_folder + "/" + finalname
|
||||
|
||||
def convert_to_mp3(self, filename, save_folder, bitrate):
|
||||
|
||||
self.source.set_property('location', filename)
|
||||
self.sink.set_property('location', self._calc_mp3_filename(filename, save_folder))
|
||||
print(bitrate)
|
||||
if bitrate < 32:
|
||||
bitrate = self.TARGET_BITRATE
|
||||
|
||||
self.encoder.set_property('bitrate', int(bitrate))
|
||||
print(bitrate)
|
||||
|
||||
# Start playing
|
||||
ret = self.converter.set_state(Gst.State.PLAYING)
|
||||
|
||||
if ret == Gst.StateChangeReturn.FAILURE:
|
||||
print("Unable to set the pipeline to the playing state.", sys.stderr)
|
||||
exit(-1)
|
||||
|
||||
# Wait until error or EOS
|
||||
bus = self.converter.get_bus()
|
||||
try:
|
||||
msg = bus.timed_pop_filtered(
|
||||
Gst.CLOCK_TIME_NONE, Gst.MessageType.ERROR | Gst.MessageType.EOS)
|
||||
except:
|
||||
# for some reason in ubuntu 12.04 Gst.CLOCK_TIME_NONE fails
|
||||
msg = bus.timed_pop_filtered(
|
||||
18446744073709551615, Gst.MessageType.ERROR | Gst.MessageType.EOS)
|
||||
|
||||
# Parse message
|
||||
if (msg):
|
||||
if msg.type == Gst.MessageType.ERROR:
|
||||
err, debug = msg.parse_error()
|
||||
print("Error received from element %s: %s" % (
|
||||
msg.src.get_name(), err), sys.stderr)
|
||||
print("Debugging information: %s" % debug, sys.stderr)
|
||||
elif msg.type == Gst.MessageType.EOS:
|
||||
print("End-Of-Stream reached.")
|
||||
else:
|
||||
print("Unexpected message received.", sys.stderr)
|
||||
|
||||
# Free resources
|
||||
self.converter.set_state(Gst.State.NULL)
|
||||
@@ -0,0 +1,244 @@
|
||||
# -*- Mode: python; coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*-
|
||||
#
|
||||
# Copyright (C) 2012 - fossfreedom
|
||||
# Copyright (C) 2012 - Agustin Carrasco
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of thie GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
from gi.repository import Peas
|
||||
from gi.repository import GObject
|
||||
import lxml.etree as ET
|
||||
|
||||
import rb
|
||||
from coverart_rb3compat import ActionGroup
|
||||
from coverart_rb3compat import ApplicationShell
|
||||
from coverart_utils import CaseInsensitiveDict
|
||||
|
||||
|
||||
class ExternalPlugin(GObject.Object):
|
||||
'''
|
||||
class for all supported ExternalPlugins
|
||||
'''
|
||||
|
||||
def __init__(self, **kargs):
|
||||
super(ExternalPlugin, self).__init__(**kargs)
|
||||
|
||||
# dict of attributes associated with the external plugin
|
||||
self.attributes = {}
|
||||
self.attributes['is_album_menu'] = False
|
||||
self.attributes['new_menu_name'] = ''
|
||||
self.attributes['action_type'] = ''
|
||||
self.attributes['action_group_name'] = ''
|
||||
|
||||
def appendattribute(self, key, val):
|
||||
'''
|
||||
append another attribute to the dict
|
||||
|
||||
:param key: `str` name of attribute
|
||||
:param val: `str` value of attribute
|
||||
'''
|
||||
|
||||
if key == 'is_album_menu':
|
||||
if val == 'yes':
|
||||
self.attributes[key] = True
|
||||
else:
|
||||
self.attributes[key] = False
|
||||
else:
|
||||
self.attributes[key] = val
|
||||
|
||||
def is_activated(self):
|
||||
'''
|
||||
method to test whether the plugin is actually loaded. Returns a bool
|
||||
'''
|
||||
peas = Peas.Engine.get_default()
|
||||
loaded_plugins = peas.get_loaded_plugins()
|
||||
|
||||
if self.attributes['plugin_name'] in CaseInsensitiveDict(loaded_plugins):
|
||||
print("found %s" % self.attributes['plugin_name'])
|
||||
return True
|
||||
|
||||
print("search for %s" % self.attributes['plugin_name'])
|
||||
print(loaded_plugins)
|
||||
|
||||
return False
|
||||
|
||||
def create_menu_item(self, menubar, section_name, at_position,
|
||||
save_actiongroup, save_menu, for_album=False):
|
||||
'''
|
||||
method to create the menu item appropriate to the plugin.
|
||||
A plugin can have many menu items - all menuitems are enclosed
|
||||
in a section.
|
||||
|
||||
:param menubar: `str` name for the GtkMenu - ignored for RB2.99
|
||||
:param section_name: `str` unique name of the section holding the menu items
|
||||
:param at_position: `int` position within the GtkMenu to create menu - ignored for RB2.99
|
||||
:param save_actiongroup: `ActionGroup` container for all menu-item Actions
|
||||
:param save_menu: `Menu` whole popupmenu including sub-menus
|
||||
:param for_album: `bool` create the menu for the album - if not given
|
||||
then its assumed the menu item is appropriate just for tracks
|
||||
'''
|
||||
if for_album and not self.attributes['is_album_menu']:
|
||||
return False
|
||||
|
||||
if not self.is_activated():
|
||||
return False
|
||||
|
||||
action = ApplicationShell(save_menu.shell).lookup_action(self.attributes['action_group_name'],
|
||||
self.attributes['action_name'],
|
||||
self.attributes['action_type'])
|
||||
|
||||
if action:
|
||||
self.attributes['action'] = action
|
||||
|
||||
if self.attributes['new_menu_name'] != '':
|
||||
self.attributes['label'] = self.attributes['new_menu_name']
|
||||
else:
|
||||
self.attributes['label'] = action.label
|
||||
# self.attributes['sensitive']=action.get_sensitive()
|
||||
else:
|
||||
print("action not found")
|
||||
print(self.attributes)
|
||||
return False
|
||||
|
||||
action = save_actiongroup.add_action(func=self.menuitem_callback,
|
||||
action_name=self.attributes['action_name'], album=for_album,
|
||||
shell=save_menu.shell, label=self.attributes['label'])
|
||||
|
||||
new_menu_item = save_menu.insert_menu_item(menubar, section_name,
|
||||
at_position, action)
|
||||
return new_menu_item
|
||||
|
||||
def do_deactivate(self):
|
||||
pass
|
||||
|
||||
def set_entry_view_selected_entries(self, shell):
|
||||
'''
|
||||
method called just before the external plugin action is activated
|
||||
|
||||
Normally only called for album menus to mimic selecting all the
|
||||
EntryView rows
|
||||
'''
|
||||
page = shell.props.selected_page
|
||||
if not hasattr(page, "get_entry_view"):
|
||||
return
|
||||
|
||||
page.get_entry_view().select_all()
|
||||
|
||||
def activate(self, shell):
|
||||
'''
|
||||
method called to initiate the external plugin action
|
||||
the action is defined by defining the action_group_name, action_name and action_type
|
||||
'''
|
||||
|
||||
action = ApplicationShell(shell).lookup_action(self.attributes['action_group_name'],
|
||||
self.attributes['action_name'],
|
||||
self.attributes['action_type'])
|
||||
|
||||
if action:
|
||||
action.activate()
|
||||
|
||||
def menuitem_callback(self, action, param, args):
|
||||
'''
|
||||
method called when a menu-item is clicked. Basically, an Action
|
||||
is activated by the user
|
||||
|
||||
:param action: `Gio.SimpleAction` or `Gtk.Action`
|
||||
:param param: Not used
|
||||
:param args: dict associated with the action
|
||||
'''
|
||||
for_album = args['album']
|
||||
shell = args['shell']
|
||||
if for_album:
|
||||
self.set_entry_view_selected_entries(shell)
|
||||
|
||||
self.attributes['action'].activate()
|
||||
|
||||
|
||||
class CreateExternalPluginMenu(GObject.Object):
|
||||
'''
|
||||
This is the key class called to initialise all supported plugins
|
||||
|
||||
:param section_name: `str` unique name of the section holding the menu items
|
||||
:param at_position: `int` position within the GtkMenu to create menu - ignored for RB2.99
|
||||
:param popup: `Menu` whole popupmenu including sub-menus
|
||||
'''
|
||||
|
||||
def __init__(self, section_name, at_position, popup, **kargs):
|
||||
super(CreateExternalPluginMenu, self).__init__(**kargs)
|
||||
|
||||
self.menu = popup
|
||||
self.section_name = section_name
|
||||
self.at_position = at_position
|
||||
|
||||
self._actiongroup = ActionGroup(popup.shell, section_name + '_externalplugins')
|
||||
|
||||
# all supported plugins will be defined in the following array by parsing
|
||||
# the plugins XML file for the definition.
|
||||
|
||||
self.supported_plugins = []
|
||||
|
||||
extplugins = rb.find_plugin_file(popup.plugin, 'ui/coverart_external_plugins.xml')
|
||||
root = ET.parse(open(extplugins)).getroot()
|
||||
|
||||
base = 'rb3/plugin'
|
||||
|
||||
for elem in root.xpath(base):
|
||||
pluginname = elem.attrib['name']
|
||||
|
||||
basemenu = base + "[@name='" + pluginname + "']/menu"
|
||||
|
||||
for menuelem in root.xpath(basemenu):
|
||||
ext = ExternalPlugin()
|
||||
ext.appendattribute('plugin_name', pluginname)
|
||||
|
||||
label = menuelem.attrib['label']
|
||||
if label != "":
|
||||
ext.appendattribute('new_menu_name', label)
|
||||
baseattrib = basemenu + "[@label='" + label + "']/attribute"
|
||||
else:
|
||||
baseattrib = basemenu + "/attribute"
|
||||
|
||||
for attribelem in root.xpath(baseattrib):
|
||||
key = attribelem.attrib['name']
|
||||
val = attribelem.text
|
||||
ext.appendattribute(key, val)
|
||||
|
||||
self.supported_plugins.append(ext)
|
||||
|
||||
def create_menu(self, menu_name, for_album=False):
|
||||
'''
|
||||
method to create the menu items for all supported plugins
|
||||
|
||||
:param menu_name: `str` unique name (GtkMenu) id for the menu to create
|
||||
:for_album: `bool` - create a menu applicable for Albums
|
||||
by default a menu is assumed to be applicable to a track in an
|
||||
EntryView
|
||||
'''
|
||||
self.menu_name = menu_name
|
||||
|
||||
self._actiongroup.remove_actions()
|
||||
self.menu.remove_menu_items(self.menu_name, self.section_name)
|
||||
|
||||
items_added = False
|
||||
|
||||
for plugin in self.supported_plugins:
|
||||
new_menu_item = plugin.create_menu_item(self.menu_name, self.section_name,
|
||||
self.at_position, self._actiongroup, self.menu, for_album)
|
||||
|
||||
if (not items_added) and new_menu_item:
|
||||
items_added = True
|
||||
|
||||
if items_added:
|
||||
self.menu.insert_separator(self.menu_name, self.at_position)
|
||||
@@ -0,0 +1,80 @@
|
||||
# -*- Mode: python; coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*-
|
||||
#
|
||||
# Copyright (C) 2012 - fossfreedom
|
||||
# Copyright (C) 2012 - Agustin Carrasco
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
from gi.repository import GObject
|
||||
from gi.repository import GLib
|
||||
|
||||
from coverart_widgets import AbstractView
|
||||
|
||||
|
||||
class ListShowingPolicy(GObject.Object):
|
||||
'''
|
||||
Policy that mostly takes care of how and when things should be showed on
|
||||
the view that makes use of the `AlbumsModel`.
|
||||
'''
|
||||
|
||||
def __init__(self, list_view):
|
||||
super(ListShowingPolicy, self).__init__()
|
||||
|
||||
self.counter = 0
|
||||
self._has_initialised = False
|
||||
|
||||
def initialise(self, album_manager):
|
||||
if self._has_initialised:
|
||||
return
|
||||
|
||||
self._has_initialised = True
|
||||
|
||||
|
||||
class ListView(AbstractView):
|
||||
__gtype_name__ = "ListView"
|
||||
|
||||
name = 'listview'
|
||||
use_plugin_window = False
|
||||
|
||||
def __init__(self):
|
||||
super(ListView, self).__init__()
|
||||
self.view = self
|
||||
self._has_initialised = False
|
||||
self.show_policy = ListShowingPolicy(self)
|
||||
|
||||
def initialise(self, source):
|
||||
if self._has_initialised:
|
||||
return
|
||||
|
||||
self._has_initialised = True
|
||||
|
||||
self.view_name = "list_view"
|
||||
super(ListView, self).initialise(source)
|
||||
# self.album_manager = source.album_manager
|
||||
self.shell = source.shell
|
||||
|
||||
def switch_to_view(self, source, album):
|
||||
self.initialise(source)
|
||||
|
||||
GLib.idle_add(self.shell.props.display_page_tree.select,
|
||||
self.shell.props.library_source)
|
||||
|
||||
def get_selected_objects(self):
|
||||
'''
|
||||
finds what has been selected
|
||||
|
||||
returns an array of `Album`
|
||||
'''
|
||||
return []
|
||||
@@ -0,0 +1,258 @@
|
||||
# -*- Mode: python; coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*-
|
||||
#
|
||||
# Copyright (C) 2012 - fossfreedom
|
||||
# Copyright (C) 2012 - Agustin Carrasco
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
# define plugin
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import RB
|
||||
from gi.repository import GObject
|
||||
from gi.repository import GLib
|
||||
from gi.repository import Gdk
|
||||
|
||||
from coverart_rb3compat import Menu
|
||||
from coverart_external_plugins import CreateExternalPluginMenu
|
||||
from coverart_entryview import CoverArtEntryView
|
||||
from coverart_rb3compat import ActionGroup
|
||||
from coverart_rb3compat import ApplicationShell
|
||||
from coverart_browser_prefs import CoverLocale
|
||||
from coverart_widgets import PressButton
|
||||
from coverart_utils import create_button_image
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
import rb
|
||||
import os
|
||||
|
||||
class CoverArtPlayEntryView(CoverArtEntryView):
|
||||
__hash__ = GObject.__hash__
|
||||
|
||||
def __init__(self, shell, source):
|
||||
'''
|
||||
Initializes the entryview.
|
||||
'''
|
||||
super(CoverArtPlayEntryView, self).__init__(shell, source)
|
||||
|
||||
def define_menu(self):
|
||||
popup = Menu(self.plugin, self.shell)
|
||||
popup.load_from_file('N/A',
|
||||
'ui/coverart_play_pop_rb3.ui')
|
||||
signals = {
|
||||
'remove_from_playlist_menu_item': self.remove_from_playlist_menu_item_callback
|
||||
}
|
||||
|
||||
popup.connect_signals(signals)
|
||||
popup.connect('pre-popup', self.pre_popup_menu_callback)
|
||||
self.popup = popup
|
||||
|
||||
def pre_popup_menu_callback(self, *args):
|
||||
'''
|
||||
Callback when the popup menu is about to be displayed
|
||||
'''
|
||||
|
||||
if not self.external_plugins:
|
||||
self.external_plugins = \
|
||||
CreateExternalPluginMenu("playlist_entry_view", 1, self.popup)
|
||||
self.external_plugins.create_menu('play_popup_menu')
|
||||
|
||||
def remove_from_playlist_menu_item_callback(self, *args):
|
||||
print("remove_from_playlist_menu_item_callback")
|
||||
entries = self.get_selected_entries()
|
||||
for entry in entries:
|
||||
print(entry)
|
||||
self.source.source_query_model.remove_entry(entry)
|
||||
|
||||
def do_show_popup(self, over_entry):
|
||||
if over_entry:
|
||||
print("CoverArtBrowser DEBUG - do_show_popup()")
|
||||
|
||||
self.popup.popup(self.source,
|
||||
'play_popup_menu', 0, Gtk.get_current_event_time())
|
||||
|
||||
return over_entry
|
||||
|
||||
def play_track_menu_item_callback(self, *args):
|
||||
print("CoverArtBrowser DEBUG - play_track_menu_item_callback()")
|
||||
|
||||
selected = self.get_selected_entries()
|
||||
entry = selected[0]
|
||||
|
||||
# Start the music
|
||||
player = self.shell.props.shell_player
|
||||
player.play_entry(entry, self.source)
|
||||
|
||||
print("CoverArtBrowser DEBUG - play_track_menu_item_callback()")
|
||||
|
||||
|
||||
class CoverArtPlaySource(RB.BrowserSource):
|
||||
def __init__(self, **kwargs):
|
||||
'''
|
||||
Initializes the source.
|
||||
'''
|
||||
super(CoverArtPlaySource, self).__init__(**kwargs)
|
||||
#self.external_plugins = None
|
||||
self.hasActivated = False
|
||||
|
||||
self.save_in_progress = False
|
||||
self.save_interrupt = False
|
||||
self.filename = RB.user_cache_dir() + "/coverart_browser/playlist.xml"
|
||||
|
||||
def do_selected(self):
|
||||
'''
|
||||
Called by Rhythmbox when the source is selected. It makes sure to
|
||||
create the ui the first time the source is showed.
|
||||
'''
|
||||
print("CoverArtBrowser DEBUG - do_selected")
|
||||
|
||||
# first time of activation -> add graphical stuff
|
||||
if not self.hasActivated:
|
||||
self.do_impl_activate()
|
||||
|
||||
# indicate that the source was activated before
|
||||
self.hasActivated = True
|
||||
|
||||
print("CoverArtBrowser DEBUG - end do_selected")
|
||||
|
||||
|
||||
def do_impl_activate(self):
|
||||
'''
|
||||
Called by do_selected the first time the source is activated.
|
||||
It creates all the source ui and connects the necessary signals for it
|
||||
correct behavior.
|
||||
'''
|
||||
print('do_impl_activate')
|
||||
|
||||
self.plugin = self.props.plugin
|
||||
self.shell = self.props.shell
|
||||
|
||||
player = self.shell.props.shell_player
|
||||
player.set_playing_source(self)
|
||||
player.set_selected_source(self)
|
||||
|
||||
|
||||
# define a query model that we'll use for playing
|
||||
self.source_query_model = self.plugin.source_query_model
|
||||
|
||||
grid = Gtk.Grid()
|
||||
|
||||
self.entryview = self.get_entry_view()
|
||||
|
||||
child = self.get_children()
|
||||
print (child)
|
||||
|
||||
grid = child[0]
|
||||
self.rbsourcetoolbar = grid.get_children()[1] # need to remember the reference to stop crashes when python cleans up unlinked objects
|
||||
grid.remove(grid.get_children()[1])
|
||||
|
||||
self.get_entry_view().set_model(self.source_query_model)
|
||||
'''
|
||||
# enable sorting on the entryview
|
||||
entryview.set_columns_clickable(True)
|
||||
self.shell.props.library_source.get_entry_view().set_columns_clickable(
|
||||
True)
|
||||
'''
|
||||
cl = CoverLocale()
|
||||
cl.switch_locale(cl.Locale.LOCALE_DOMAIN)
|
||||
location = rb.find_plugin_file(self.plugin, 'ui/playsource-toolbar.ui')
|
||||
ui = Gtk.Builder()
|
||||
ui.set_translation_domain(cl.Locale.RB)
|
||||
ui.add_from_file(location)
|
||||
toolbar_menu = ui.get_object('playsource-toolbar')
|
||||
app = self.shell.props.application
|
||||
app.link_shared_menus(toolbar_menu)
|
||||
self.toolbar = RB.ButtonBar.new(toolbar_menu, toolbar_menu)
|
||||
self.toolbar.props.hexpand_set = False
|
||||
grid.attach(self.toolbar, 0, 0, 1, 1)
|
||||
|
||||
grid.show_all()
|
||||
|
||||
appshell = ApplicationShell(self.shell)
|
||||
action_group = ActionGroup(self.shell, 'PlaySourceActions')
|
||||
action_group.add_action(func=self.clear_playsource,
|
||||
action_name='playsource-clear', action_state=ActionGroup.STANDARD,
|
||||
action_type='app')
|
||||
action_group.add_action(func=self.shuffle_playsource,
|
||||
action_name='playsource-shuffle', action_state=ActionGroup.STANDARD,
|
||||
action_type='app')
|
||||
appshell.insert_action_group(action_group)
|
||||
|
||||
|
||||
# if the alternative-toolbar is loaded then lets connect to the toolbar-visibility signal
|
||||
# to control our sources toolbar visibility
|
||||
|
||||
#if hasattr(self.shell, 'alternative_toolbar'):
|
||||
# self.shell.alternative_toolbar.connect('toolbar-visibility', self._visibility)
|
||||
|
||||
self._load_model()
|
||||
|
||||
self.source_query_model.connect('row-inserted', self.save_changed_model)
|
||||
self.source_query_model.connect('row-changed', self.save_changed_model)
|
||||
self.source_query_model.connect('row-deleted', self.save_changed_model)
|
||||
|
||||
def _load_model(self):
|
||||
if not os.path.isfile(self.filename):
|
||||
return
|
||||
|
||||
parser = ET.XMLParser(encoding="utf-8")
|
||||
tree = ET.parse(self.filename, parser=parser)
|
||||
|
||||
root = tree.getroot()
|
||||
|
||||
for child in root.findall('./entry/text'):
|
||||
location = child.text
|
||||
entry = self.shell.props.db.entry_lookup_by_location(location)
|
||||
if entry:
|
||||
self.source_query_model.add_entry(entry, -1)
|
||||
|
||||
self.props.query_model = self.source_query_model
|
||||
|
||||
def clear_playsource(self, *args):
|
||||
for row in self.get_entry_view().props.model:
|
||||
self.get_entry_view().props.model.remove_entry(row[0])
|
||||
|
||||
def shuffle_playsource(self, *args):
|
||||
self.get_entry_view().props.model.shuffle_entries()
|
||||
self._save_model()
|
||||
|
||||
def save_changed_model(self, *args):
|
||||
|
||||
if self.save_in_progress:
|
||||
self.save_interrupt = True
|
||||
return
|
||||
|
||||
self.save_in_progress = True
|
||||
|
||||
Gdk.threads_add_timeout_seconds(GLib.PRIORITY_DEFAULT_IDLE, 1, self._save_model, None)
|
||||
|
||||
def _save_model(self, *args):
|
||||
if self.save_interrupt:
|
||||
self.save_interrupt = False
|
||||
return True
|
||||
|
||||
root = ET.Element('root')
|
||||
element = ET.SubElement(root, 'entry')
|
||||
for row in self.source_query_model:
|
||||
location = row[0].get_string(RB.RhythmDBPropType.LOCATION)
|
||||
subelement = ET.SubElement(element, 'text')
|
||||
subelement.text = location
|
||||
|
||||
tree = ET.ElementTree(root)
|
||||
tree.write(self.filename)
|
||||
|
||||
self.save_in_progress = False
|
||||
return False
|
||||
|
||||
GObject.type_register(CoverArtPlayEntryView)
|
||||
@@ -0,0 +1,377 @@
|
||||
# -*- Mode: python; coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*-
|
||||
#
|
||||
# Copyright (C) 2012 - fossfreedom
|
||||
# Copyright (C) 2012 - Agustin Carrasco
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
import urllib.parse
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
|
||||
from gi.repository import RB
|
||||
from gi.repository import Gtk
|
||||
|
||||
from coverart_utils import idle_iterator
|
||||
import rb
|
||||
|
||||
|
||||
LOAD_CHUNK = 50
|
||||
|
||||
|
||||
class WebPlaylist(object):
|
||||
MAX_TRACKS_TO_ADD = 3 # number of tracks to add to a source for each fetch
|
||||
MIN_TRACKS_TO_FETCH = 5 # number of tracks in source before a fetch will be required
|
||||
TOTAL_TRACKS_REMEMBERED = 25 # total number of tracks for all artists before a fetch is allowed
|
||||
MAX_TRACKS_PER_ARTIST = 3 # number of tracks allowed to be remembered per artist
|
||||
|
||||
def __init__(self, shell, source, playlist_name):
|
||||
|
||||
self.shell = shell
|
||||
# lets fill up the queue with artists
|
||||
self.candidate_artist = {}
|
||||
self.shell.props.shell_player.connect('playing-song-changed', self.playing_song_changed)
|
||||
self.source = source
|
||||
self.search_entry = None
|
||||
self.playlist_started = False
|
||||
self.played_artist = {}
|
||||
self.tracks_not_played = 0
|
||||
# cache for artist information: valid for a month, can be used indefinitely
|
||||
# if offline, discarded if unused for six months
|
||||
self.info_cache = rb.URLCache(name=playlist_name,
|
||||
path=os.path.join('coverart_browser', playlist_name),
|
||||
refresh=30,
|
||||
discard=180)
|
||||
self.info_cache.clean()
|
||||
|
||||
def playing_song_changed(self, player, entry):
|
||||
if not entry:
|
||||
return
|
||||
|
||||
if player.get_playing_source() != self.source:
|
||||
self.playlist_started = False
|
||||
self.played_artist.clear()
|
||||
self.tracks_not_played = 0
|
||||
|
||||
if self.playlist_started and len(self.source.props.query_model) < self.MIN_TRACKS_TO_FETCH:
|
||||
self.start(entry)
|
||||
|
||||
def start(self, seed_entry, reinitialise=False):
|
||||
artist = seed_entry.get_string(RB.RhythmDBPropType.ARTIST)
|
||||
|
||||
if reinitialise:
|
||||
self.played_artist.clear()
|
||||
self.tracks_not_played = 0
|
||||
self.playlist_started = False
|
||||
|
||||
player = self.shell.props.shell_player
|
||||
_, is_playing = player.get_playing()
|
||||
|
||||
if is_playing:
|
||||
player.stop()
|
||||
|
||||
for row in self.source.props.query_model:
|
||||
self.source.props.query_model.remove_entry(row[0])
|
||||
|
||||
if self.tracks_not_played > self.TOTAL_TRACKS_REMEMBERED:
|
||||
print(("we have plenty of tracks to play yet - no need to fetch more %d", self.tracks_not_played))
|
||||
self.add_tracks_to_source()
|
||||
return
|
||||
|
||||
search_artist = urllib.parse.quote(artist.encode("utf8"))
|
||||
if search_artist in self.played_artist:
|
||||
print("we have already searched for that artist")
|
||||
return
|
||||
|
||||
self.search_entry = seed_entry
|
||||
self.played_artist[search_artist] = True
|
||||
|
||||
self.playlist_started = True
|
||||
self._running = False
|
||||
self._start_process()
|
||||
|
||||
def _start_process(self):
|
||||
if not self._running:
|
||||
self._running = True
|
||||
self.search_website()
|
||||
|
||||
def search_website(self):
|
||||
pass
|
||||
|
||||
def _clear_next(self):
|
||||
self.search_artists = ""
|
||||
self._running = False
|
||||
|
||||
@idle_iterator
|
||||
def _load_albums(self):
|
||||
def process(row, data):
|
||||
entry = data['model'][row.path][0]
|
||||
|
||||
lookup = entry.get_string(RB.RhythmDBPropType.ARTIST_FOLDED)
|
||||
lookup_title = entry.get_string(RB.RhythmDBPropType.TITLE_FOLDED)
|
||||
|
||||
if lookup in self.artist and \
|
||||
lookup_title in \
|
||||
self.artist[lookup]:
|
||||
|
||||
if lookup not in self.candidate_artist:
|
||||
self.candidate_artist[lookup] = []
|
||||
|
||||
# N.B. every artist has an array of dicts with a known format of track & add-to-source elements
|
||||
# the following extracts the track-title and add-to-source to form a dict of track-title and a value
|
||||
# of the add-to-source
|
||||
d = dict((i['track-title'], i['add-to-source']) for i in self.candidate_artist[lookup])
|
||||
if len(d) < self.MAX_TRACKS_PER_ARTIST and lookup_title not in d:
|
||||
# we only append a max of three tracks to each artist
|
||||
self.candidate_artist[lookup].append({
|
||||
'track': entry,
|
||||
'add-to-source': False,
|
||||
'track-title': lookup_title})
|
||||
self.tracks_not_played = self.tracks_not_played + 1
|
||||
|
||||
|
||||
def after(data):
|
||||
# update the progress
|
||||
pass
|
||||
|
||||
def error(exception):
|
||||
print(('Error processing entries: ' + str(exception)))
|
||||
|
||||
def finish(data):
|
||||
|
||||
self.add_tracks_to_source()
|
||||
self._clear_next()
|
||||
|
||||
return LOAD_CHUNK, process, after, error, finish
|
||||
|
||||
def display_error_message(self):
|
||||
dialog = Gtk.MessageDialog(None,
|
||||
Gtk.DialogFlags.MODAL,
|
||||
Gtk.MessageType.INFO,
|
||||
Gtk.ButtonsType.OK,
|
||||
_("No matching tracks have been found"))
|
||||
|
||||
dialog.run()
|
||||
dialog.destroy()
|
||||
|
||||
def add_tracks_to_source(self):
|
||||
entries = []
|
||||
for artist in self.candidate_artist:
|
||||
|
||||
d = dict((i['track'], (self.candidate_artist[artist].index(i),
|
||||
i['add-to-source'],
|
||||
artist)) for i in self.candidate_artist[artist])
|
||||
|
||||
for entry, elements in d.items():
|
||||
element_pos, add_to_source, artist = elements
|
||||
if not add_to_source:
|
||||
entries.append({entry: elements})
|
||||
|
||||
random.shuffle(entries)
|
||||
|
||||
count = 0
|
||||
for row in entries:
|
||||
print(row)
|
||||
entry, elements = list(row.items())[0]
|
||||
element_pos, add_to_source, artist = elements
|
||||
self.source.add_entry(entry, -1)
|
||||
self.candidate_artist[artist][element_pos]['add-to-source'] = True
|
||||
|
||||
count = count + 1
|
||||
self.tracks_not_played = self.tracks_not_played - 1
|
||||
if count == self.MAX_TRACKS_TO_ADD:
|
||||
break
|
||||
|
||||
player = self.shell.props.shell_player
|
||||
|
||||
_, is_playing = player.get_playing()
|
||||
|
||||
if len(self.source.props.query_model) > 0 and not is_playing:
|
||||
player.play_entry(self.source.props.query_model[0][0], self.source)
|
||||
|
||||
|
||||
class LastFMTrackPlaylist(WebPlaylist):
|
||||
def __init__(self, shell, source):
|
||||
WebPlaylist.__init__(self, shell, source, "lastfm_trackplaylist")
|
||||
|
||||
def search_website(self):
|
||||
# unless already cached - directly fetch from lastfm similar track information
|
||||
apikey = "844353bce568b93accd9ca47674d6c3e"
|
||||
url = "http://ws.audioscrobbler.com/2.0/?method=track.getsimilar&api_key={0}&artist={1}&track={2}&format=json"
|
||||
|
||||
artist = self.search_entry.get_string(RB.RhythmDBPropType.ARTIST)
|
||||
title = self.search_entry.get_string(RB.RhythmDBPropType.TITLE)
|
||||
artist = urllib.parse.quote(artist.encode("utf8"))
|
||||
title = urllib.parse.quote(title.encode("utf8"))
|
||||
formatted_url = url.format(urllib.parse.quote(apikey),
|
||||
artist,
|
||||
title)
|
||||
|
||||
print(formatted_url)
|
||||
cachekey = "artist:%s:title:%s" % (artist, title)
|
||||
self.info_cache.fetch(cachekey, formatted_url, self.similar_info_cb, None)
|
||||
|
||||
def similar_info_cb(self, data, _):
|
||||
|
||||
if not data:
|
||||
print("nothing to do")
|
||||
self.display_error_message()
|
||||
self._clear_next()
|
||||
return
|
||||
|
||||
similar = json.loads(data.decode('utf-8'))
|
||||
|
||||
# loop through the response and find all titles for the artists returned
|
||||
self.artist = {}
|
||||
|
||||
if 'similartracks' not in similar:
|
||||
print("No matching data returned from LastFM")
|
||||
self.display_error_message()
|
||||
self._clear_next()
|
||||
return
|
||||
for song in similar['similartracks']['track']:
|
||||
name = RB.search_fold(song['artist']['name'])
|
||||
if name not in self.artist:
|
||||
self.artist[name] = []
|
||||
|
||||
self.artist[name].append(RB.search_fold(song['name']))
|
||||
|
||||
if len(self.artist) == 0:
|
||||
print("no artists returned")
|
||||
self._clear_next()
|
||||
return
|
||||
|
||||
# loop through every track - see if the track contains the artist & title
|
||||
# if yes then this is a candidate similar track to remember
|
||||
|
||||
query_model = self.shell.props.library_source.props.base_query_model
|
||||
|
||||
self._load_albums(iter(query_model), albums={}, model=query_model,
|
||||
total=len(query_model), progress=0.)
|
||||
|
||||
|
||||
class EchoNestPlaylist(WebPlaylist):
|
||||
def __init__(self, shell, source):
|
||||
WebPlaylist.__init__(self, shell, source, "echonest_playlist")
|
||||
|
||||
def search_website(self):
|
||||
# unless already cached - directly fetch from echonest similar artist information
|
||||
apikey = "N685TONJGZSHBDZMP"
|
||||
url = "http://developer.echonest.com/api/v4/playlist/basic?api_key={0}&artist={1}&format=json&results=100&type=artist-radio&limited_interactivity=true"
|
||||
|
||||
artist = self.search_entry.get_string(RB.RhythmDBPropType.ARTIST)
|
||||
artist = urllib.parse.quote(artist.encode("utf8"))
|
||||
formatted_url = url.format(urllib.parse.quote(apikey),
|
||||
artist)
|
||||
|
||||
print(formatted_url)
|
||||
cachekey = "artist:%s" % artist
|
||||
self.info_cache.fetch(cachekey, formatted_url, self.similar_info_cb, None)
|
||||
|
||||
def similar_info_cb(self, data, _):
|
||||
|
||||
if not data:
|
||||
print("nothing to do")
|
||||
self.display_error_message()
|
||||
self._clear_next()
|
||||
return
|
||||
|
||||
similar = json.loads(data.decode('utf-8'))
|
||||
|
||||
# loop through the response and find all titles for the artists returned
|
||||
self.artist = {}
|
||||
|
||||
if 'songs' not in similar['response']:
|
||||
print("No matching data returned from EchoNest")
|
||||
self.display_error_message()
|
||||
self._clear_next()
|
||||
return
|
||||
for song in similar['response']['songs']:
|
||||
name = RB.search_fold(song['artist_name'])
|
||||
if name not in self.artist:
|
||||
self.artist[name] = []
|
||||
|
||||
self.artist[name].append(RB.search_fold(song['title']))
|
||||
|
||||
if len(self.artist) == 0:
|
||||
print("no artists returned")
|
||||
self._clear_next()
|
||||
return
|
||||
|
||||
# loop through every track - see if the track contains the artist & title
|
||||
# if yes then this is a candidate similar track to remember
|
||||
|
||||
query_model = self.shell.props.library_source.props.base_query_model
|
||||
|
||||
self._load_albums(iter(query_model), albums={}, model=query_model,
|
||||
total=len(query_model), progress=0.)
|
||||
|
||||
|
||||
class EchoNestGenrePlaylist(WebPlaylist):
|
||||
def __init__(self, shell, source):
|
||||
WebPlaylist.__init__(self, shell, source, "echonest_genre_playlist")
|
||||
|
||||
def search_website(self):
|
||||
# unless already cached - directly fetch from echonest similar artist information
|
||||
apikey = "N685TONJGZSHBDZMP"
|
||||
url = "http://developer.echonest.com/api/v4/playlist/basic?api_key={0}&genre={1}&format=json&results=100&type=genre-radio&limited_interactivity=true"
|
||||
|
||||
genre = self.search_entry.get_string(RB.RhythmDBPropType.GENRE).lower()
|
||||
genre = urllib.parse.quote(genre.encode("utf8"))
|
||||
formatted_url = url.format(urllib.parse.quote(apikey),
|
||||
genre)
|
||||
|
||||
print(formatted_url)
|
||||
cachekey = "genre:%s" % genre
|
||||
self.info_cache.fetch(cachekey, formatted_url, self.similar_info_cb, None)
|
||||
|
||||
def similar_info_cb(self, data, _):
|
||||
|
||||
if not data:
|
||||
print("nothing to do")
|
||||
self.display_error_message()
|
||||
self._clear_next()
|
||||
return
|
||||
|
||||
similar = json.loads(data.decode('utf-8'))
|
||||
|
||||
# loop through the response and find all titles for the artists returned
|
||||
self.artist = {}
|
||||
|
||||
if 'songs' not in similar['response']:
|
||||
print("No matching data returned from EchoNest")
|
||||
self.display_error_message()
|
||||
self._clear_next()
|
||||
return
|
||||
for song in similar['response']['songs']:
|
||||
name = RB.search_fold(song['artist_name'])
|
||||
if name not in self.artist:
|
||||
self.artist[name] = []
|
||||
|
||||
self.artist[name].append(RB.search_fold(song['title']))
|
||||
|
||||
if len(self.artist) == 0:
|
||||
print("no artists returned")
|
||||
self._clear_next()
|
||||
return
|
||||
|
||||
# loop through every track - see if the track contains the artist & title
|
||||
# if yes then this is a candidate similar track to remember
|
||||
|
||||
query_model = self.shell.props.library_source.props.base_query_model
|
||||
|
||||
self._load_albums(iter(query_model), albums={}, model=query_model,
|
||||
total=len(query_model), progress=0.)
|
||||
@@ -0,0 +1,80 @@
|
||||
# -*- Mode: python; coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*-
|
||||
#
|
||||
# Copyright (C) 2012 - fossfreedom
|
||||
# Copyright (C) 2012 - Agustin Carrasco
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
from gi.repository import GObject
|
||||
from gi.repository import GLib
|
||||
|
||||
from coverart_widgets import AbstractView
|
||||
|
||||
|
||||
class PlaySourceShowingPolicy(GObject.Object):
|
||||
'''
|
||||
Policy that mostly takes care of how and when things should be showed on
|
||||
the view that makes use of the `AlbumsModel`.
|
||||
'''
|
||||
|
||||
def __init__(self, list_view):
|
||||
super(PlaySourceShowingPolicy, self).__init__()
|
||||
|
||||
self.counter = 0
|
||||
self._has_initialised = False
|
||||
|
||||
def initialise(self, album_manager):
|
||||
if self._has_initialised:
|
||||
return
|
||||
|
||||
self._has_initialised = True
|
||||
|
||||
|
||||
class PlaySourceView(AbstractView):
|
||||
__gtype_name__ = "PlaySourceView"
|
||||
|
||||
name = 'playsourceview'
|
||||
use_plugin_window = False
|
||||
|
||||
def __init__(self):
|
||||
super(PlaySourceView, self).__init__()
|
||||
self.view = self
|
||||
self._has_initialised = False
|
||||
self.show_policy = PlaySourceShowingPolicy(self)
|
||||
|
||||
def initialise(self, source):
|
||||
if self._has_initialised:
|
||||
return
|
||||
|
||||
self._has_initialised = True
|
||||
|
||||
self.view_name = "playsource_view"
|
||||
super(PlaySourceView, self).initialise(source)
|
||||
# self.album_manager = source.album_manager
|
||||
self.shell = source.shell
|
||||
|
||||
def switch_to_view(self, source, album):
|
||||
self.initialise(source)
|
||||
|
||||
GLib.idle_add(self.shell.props.display_page_tree.select,
|
||||
source.plugin.playlist_source)
|
||||
|
||||
def get_selected_objects(self):
|
||||
'''
|
||||
finds what has been selected
|
||||
|
||||
returns an array of `Album`
|
||||
'''
|
||||
return []
|
||||
@@ -0,0 +1,80 @@
|
||||
# -*- Mode: python; coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*-
|
||||
#
|
||||
# Copyright (C) 2012 - fossfreedom
|
||||
# Copyright (C) 2012 - Agustin Carrasco
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
from gi.repository import GObject
|
||||
from gi.repository import GLib
|
||||
|
||||
from coverart_widgets import AbstractView
|
||||
|
||||
|
||||
class QueueShowingPolicy(GObject.Object):
|
||||
'''
|
||||
Policy that mostly takes care of how and when things should be showed on
|
||||
the view that makes use of the `AlbumsModel`.
|
||||
'''
|
||||
|
||||
def __init__(self, list_view):
|
||||
super(QueueShowingPolicy, self).__init__()
|
||||
|
||||
self.counter = 0
|
||||
self._has_initialised = False
|
||||
|
||||
def initialise(self, album_manager):
|
||||
if self._has_initialised:
|
||||
return
|
||||
|
||||
self._has_initialised = True
|
||||
|
||||
|
||||
class QueueView(AbstractView):
|
||||
__gtype_name__ = "QueueView"
|
||||
|
||||
name = 'queueview'
|
||||
use_plugin_window = False
|
||||
|
||||
def __init__(self):
|
||||
super(QueueView, self).__init__()
|
||||
self.view = self
|
||||
self._has_initialised = False
|
||||
self.show_policy = QueueShowingPolicy(self)
|
||||
|
||||
def initialise(self, source):
|
||||
if self._has_initialised:
|
||||
return
|
||||
|
||||
self._has_initialised = True
|
||||
|
||||
self.view_name = "queue_view"
|
||||
super(QueueView, self).initialise(source)
|
||||
# self.album_manager = source.album_manager
|
||||
self.shell = source.shell
|
||||
|
||||
def switch_to_view(self, source, album):
|
||||
self.initialise(source)
|
||||
|
||||
GLib.idle_add(self.shell.props.display_page_tree.select,
|
||||
self.shell.props.queue_source)
|
||||
|
||||
def get_selected_objects(self):
|
||||
'''
|
||||
finds what has been selected
|
||||
|
||||
returns an array of `Album`
|
||||
'''
|
||||
return []
|
||||
@@ -0,0 +1,887 @@
|
||||
# -*- Mode: python; coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*-
|
||||
#
|
||||
# IMPORTANT - WHILST THIS MODULE IS USED BY SEVERAL OTHER PLUGINS
|
||||
# THE MASTER AND MOST UP-TO-DATE IS FOUND IN THE COVERART BROWSER
|
||||
# PLUGIN - https://github.com/fossfreedom/coverart-browser
|
||||
# PLEASE SUBMIT CHANGES BACK TO HELP EXPAND THIS API
|
||||
#
|
||||
# Copyright (C) 2012 - fossfreedom
|
||||
# Copyright (C) 2012 - Agustin Carrasco
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import Gio
|
||||
from gi.repository import GLib
|
||||
from gi.repository import GObject
|
||||
from gi.repository import RB
|
||||
|
||||
import rb
|
||||
|
||||
def gtk_version():
|
||||
'''
|
||||
returns float of the major and minor parts of the GTK version
|
||||
e.g. return float(3.10)
|
||||
'''
|
||||
|
||||
return float(str(Gtk.get_major_version())+"."+str(Gtk.get_minor_version()))
|
||||
|
||||
def pygobject_version():
|
||||
'''
|
||||
returns float of the major and minor parts of a pygobject version
|
||||
e.g. version (3, 9, 5) return float(3.9)
|
||||
'''
|
||||
to_number = lambda t: ".".join(str(v) for v in t)
|
||||
|
||||
str_version = to_number(GObject.pygobject_version)
|
||||
|
||||
return float(str_version.rsplit('.', 1)[0])
|
||||
|
||||
|
||||
def compare_pygobject_version(version):
|
||||
'''
|
||||
return True if version is less than pygobject_version
|
||||
i.e. 3.9 < 3.11
|
||||
'''
|
||||
to_number = lambda t: ".".join(str(v) for v in t)
|
||||
|
||||
str_version = to_number(GObject.pygobject_version)
|
||||
|
||||
split = str_version.rsplit('.', 2)
|
||||
split_compare = version.rsplit('.', 2)
|
||||
|
||||
if int(split_compare[0]) < int(split[0]):
|
||||
return True
|
||||
|
||||
if int(split_compare[1]) < int(split[1]):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
PYVER = sys.version_info[0]
|
||||
|
||||
if PYVER >= 3:
|
||||
import urllib.request, urllib.parse, urllib.error
|
||||
else:
|
||||
import urllib
|
||||
from urlparse import urlparse as rb2urlparse
|
||||
|
||||
if PYVER >= 3:
|
||||
import http.client
|
||||
else:
|
||||
import httplib
|
||||
|
||||
|
||||
def responses():
|
||||
if PYVER >= 3:
|
||||
return http.client.responses
|
||||
else:
|
||||
return httplib.responses
|
||||
|
||||
|
||||
def unicodestr(param, charset):
|
||||
if PYVER >= 3:
|
||||
return param # str(param, charset)
|
||||
else:
|
||||
return unicode(param, charset)
|
||||
|
||||
|
||||
def unicodeencode(param, charset):
|
||||
if PYVER >= 3:
|
||||
return param # str(param).encode(charset)
|
||||
else:
|
||||
return unicode(param).encode(charset)
|
||||
|
||||
|
||||
def unicodedecode(param, charset):
|
||||
if PYVER >= 3:
|
||||
return param
|
||||
else:
|
||||
return param.decode(charset)
|
||||
|
||||
|
||||
def urlparse(uri):
|
||||
if PYVER >= 3:
|
||||
return urllib.parse.urlparse(uri)
|
||||
else:
|
||||
return rb2urlparse(uri)
|
||||
|
||||
|
||||
def url2pathname(url):
|
||||
if PYVER >= 3:
|
||||
return urllib.request.url2pathname(url)
|
||||
else:
|
||||
return urllib.url2pathname(url)
|
||||
|
||||
|
||||
def urlopen(filename):
|
||||
if PYVER >= 3:
|
||||
return urllib.request.urlopen(filename)
|
||||
else:
|
||||
return urllib.urlopen(filename)
|
||||
|
||||
|
||||
def pathname2url(filename):
|
||||
if PYVER >= 3:
|
||||
return urllib.request.pathname2url(filename)
|
||||
else:
|
||||
return urllib.pathname2url(filename)
|
||||
|
||||
|
||||
def unquote(uri):
|
||||
if PYVER >= 3:
|
||||
return urllib.parse.unquote(uri)
|
||||
else:
|
||||
return urllib.unquote(uri)
|
||||
|
||||
|
||||
def quote(uri, safe=None):
|
||||
if PYVER >= 3:
|
||||
if safe:
|
||||
return urllib.parse.quote(uri, safe=safe)
|
||||
else:
|
||||
return urllib.parse.quote(uri)
|
||||
else:
|
||||
if safe:
|
||||
return urllib.quote(uri, safe=safe)
|
||||
else:
|
||||
return urllib.quote(uri)
|
||||
|
||||
|
||||
def quote_plus(uri):
|
||||
if PYVER >= 3:
|
||||
return urllib.parse.quote_plus(uri)
|
||||
else:
|
||||
return urllib.quote_plus(uri)
|
||||
|
||||
|
||||
def is_rb3(*args):
|
||||
if hasattr(RB.Shell.props, 'ui_manager'):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
class Menu(GObject.Object):
|
||||
'''
|
||||
Menu object used to create window popup menus
|
||||
'''
|
||||
__gsignals__ = {
|
||||
'pre-popup': (GObject.SIGNAL_RUN_LAST, None, ())
|
||||
}
|
||||
|
||||
def __init__(self, plugin, shell):
|
||||
'''
|
||||
Initializes the menu.
|
||||
'''
|
||||
super(Menu, self).__init__()
|
||||
self.plugin = plugin
|
||||
self.shell = shell
|
||||
self._unique_num = 0
|
||||
|
||||
self._rbmenu_items = {}
|
||||
self._rbmenu_objects = {}
|
||||
|
||||
def add_menu_item(self, menubar, section_name, action):
|
||||
'''
|
||||
add a new menu item to the popup
|
||||
:param menubar: `str` is the name GtkMenu (or ignored for RB2.99+)
|
||||
:param section_name: `str` is the name of the section to add the item to (RB2.99+)
|
||||
:param action: `Action` to associate with the menu item
|
||||
'''
|
||||
return self.insert_menu_item(menubar, section_name, -1, action)
|
||||
|
||||
def insert_menu_item(self, menubar, section_name, position, action):
|
||||
'''
|
||||
add a new menu item to the popup
|
||||
:param menubar: `str` is the name GtkMenu (or ignored for RB2.99+)
|
||||
:param section_name: `str` is the name of the section to add the item to (RB2.99+)
|
||||
:param position: `int` position to add to GtkMenu (ignored for RB2.99+)
|
||||
:param action: `Action` to associate with the menu item
|
||||
'''
|
||||
label = action.label
|
||||
|
||||
if is_rb3(self.shell):
|
||||
app = self.shell.props.application
|
||||
item = Gio.MenuItem()
|
||||
action.associate_menuitem(item)
|
||||
item.set_label(label)
|
||||
|
||||
if not section_name in self._rbmenu_items:
|
||||
self._rbmenu_items[section_name] = []
|
||||
self._rbmenu_items[section_name].append(label)
|
||||
|
||||
app.add_plugin_menu_item(section_name, label, item)
|
||||
else:
|
||||
item = Gtk.MenuItem(label=label)
|
||||
action.associate_menuitem(item)
|
||||
self._rbmenu_items[label] = item
|
||||
bar = self.get_menu_object(menubar)
|
||||
|
||||
if position == -1:
|
||||
bar.append(item)
|
||||
else:
|
||||
bar.insert(item, position)
|
||||
bar.show_all()
|
||||
uim = self.shell.props.ui_manager
|
||||
uim.ensure_update()
|
||||
|
||||
return item
|
||||
|
||||
def insert_separator(self, menubar, at_position):
|
||||
'''
|
||||
add a separator to the popup (only required for RB2.98 and earlier)
|
||||
:param menubar: `str` is the name GtkMenu (or ignored for RB2.99+)
|
||||
:param position: `int` position to add to GtkMenu (ignored for RB2.99+)
|
||||
'''
|
||||
if not is_rb3(self.shell):
|
||||
menu_item = Gtk.SeparatorMenuItem().new()
|
||||
menu_item.set_visible(True)
|
||||
self._rbmenu_items['separator' + str(self._unique_num)] = menu_item
|
||||
self._unique_num = self._unique_num + 1
|
||||
bar = self.get_menu_object(menubar)
|
||||
bar.insert(menu_item, at_position)
|
||||
bar.show_all()
|
||||
uim = self.shell.props.ui_manager
|
||||
uim.ensure_update()
|
||||
|
||||
def remove_menu_items(self, menubar, section_name):
|
||||
'''
|
||||
utility function to remove all menuitems associated with the menu section
|
||||
:param menubar: `str` is the name of the GtkMenu containing the menu items (ignored for RB2.99+)
|
||||
:param section_name: `str` is the name of the section containing the menu items (for RB2.99+ only)
|
||||
'''
|
||||
if is_rb3(self.shell):
|
||||
if not section_name in self._rbmenu_items:
|
||||
return
|
||||
|
||||
app = self.shell.props.application
|
||||
|
||||
for menu_item in self._rbmenu_items[section_name]:
|
||||
app.remove_plugin_menu_item(section_name, menu_item)
|
||||
|
||||
if self._rbmenu_items[section_name]:
|
||||
del self._rbmenu_items[section_name][:]
|
||||
|
||||
else:
|
||||
|
||||
if not self._rbmenu_items:
|
||||
return
|
||||
|
||||
uim = self.shell.props.ui_manager
|
||||
bar = self.get_menu_object(menubar)
|
||||
|
||||
for menu_item in self._rbmenu_items:
|
||||
bar.remove(self._rbmenu_items[menu_item])
|
||||
|
||||
bar.show_all()
|
||||
uim.ensure_update()
|
||||
|
||||
def load_from_file(self, rb2_ui_filename, rb3_ui_filename):
|
||||
'''
|
||||
utility function to load the menu structure
|
||||
:param rb2_ui_filename: `str` RB2.98 and below UI file
|
||||
:param rb3_ui_filename: `str` RB2.99 and higher UI file
|
||||
'''
|
||||
self.builder = Gtk.Builder()
|
||||
try:
|
||||
from coverart_browser_prefs import CoverLocale
|
||||
|
||||
cl = CoverLocale()
|
||||
|
||||
self.builder.set_translation_domain(cl.Locale.LOCALE_DOMAIN)
|
||||
except:
|
||||
pass
|
||||
|
||||
if is_rb3(self.shell):
|
||||
ui_filename = rb3_ui_filename
|
||||
else:
|
||||
ui_filename = rb2_ui_filename
|
||||
|
||||
self.ui_filename = ui_filename
|
||||
|
||||
self.builder.add_from_file(rb.find_plugin_file(self.plugin,
|
||||
ui_filename))
|
||||
|
||||
def _connect_rb3_signals(self, signals):
|
||||
def _menu_connect(action_name, func):
|
||||
action = Gio.SimpleAction(name=action_name)
|
||||
action.connect('activate', func)
|
||||
action.set_enabled(True)
|
||||
self.shell.props.window.add_action(action)
|
||||
|
||||
for key, value in signals.items():
|
||||
_menu_connect(key, value)
|
||||
|
||||
def _connect_rb2_signals(self, signals):
|
||||
def _menu_connect(menu_item_name, func):
|
||||
menu_item = self.get_menu_object(menu_item_name)
|
||||
menu_item.connect('activate', func)
|
||||
|
||||
for key, value in signals.items():
|
||||
_menu_connect(key, value)
|
||||
|
||||
def connect_signals(self, signals):
|
||||
'''
|
||||
connect all signal handlers with their menuitem counterparts
|
||||
:param signals: `dict` key is the name of the menuitem
|
||||
and value is the function callback when the menu is activated
|
||||
'''
|
||||
if is_rb3(self.shell):
|
||||
self._connect_rb3_signals(signals)
|
||||
else:
|
||||
self._connect_rb2_signals(signals)
|
||||
|
||||
def get_gtkmenu(self, source, popup_name):
|
||||
'''
|
||||
utility function to obtain the GtkMenu from the menu UI file
|
||||
:param popup_name: `str` is the name menu-id in the UI file
|
||||
'''
|
||||
if popup_name in self._rbmenu_objects:
|
||||
return self._rbmenu_objects[popup_name]
|
||||
item = self.builder.get_object(popup_name)
|
||||
|
||||
if is_rb3(self.shell):
|
||||
app = self.shell.props.application
|
||||
app.link_shared_menus(item)
|
||||
popup_menu = Gtk.Menu.new_from_model(item)
|
||||
popup_menu.attach_to_widget(source, None)
|
||||
else:
|
||||
popup_menu = item
|
||||
|
||||
self._rbmenu_objects[popup_name] = popup_menu
|
||||
|
||||
return popup_menu
|
||||
|
||||
def get_menu_object(self, menu_name_or_link):
|
||||
'''
|
||||
utility function returns the GtkMenuItem/Gio.MenuItem
|
||||
:param menu_name_or_link: `str` to search for in the UI file
|
||||
'''
|
||||
if menu_name_or_link in self._rbmenu_objects:
|
||||
return self._rbmenu_objects[menu_name_or_link]
|
||||
item = self.builder.get_object(menu_name_or_link)
|
||||
if is_rb3(self.shell):
|
||||
if item:
|
||||
popup_menu = item
|
||||
else:
|
||||
app = self.shell.props.application
|
||||
popup_menu = app.get_plugin_menu(menu_name_or_link)
|
||||
else:
|
||||
popup_menu = item
|
||||
print(menu_name_or_link)
|
||||
self._rbmenu_objects[menu_name_or_link] = popup_menu
|
||||
|
||||
return popup_menu
|
||||
|
||||
def set_sensitive(self, menu_or_action_item, enable):
|
||||
'''
|
||||
utility function to enable/disable a menu-item
|
||||
:param menu_or_action_item: `GtkMenuItem` or `Gio.SimpleAction`
|
||||
that is to be enabled/disabled
|
||||
:param enable: `bool` value to enable/disable
|
||||
'''
|
||||
|
||||
if is_rb3(self.shell):
|
||||
item = self.shell.props.window.lookup_action(menu_or_action_item)
|
||||
item.set_enabled(enable)
|
||||
else:
|
||||
item = self.get_menu_object(menu_or_action_item)
|
||||
item.set_sensitive(enable)
|
||||
|
||||
def popup(self, source, menu_name, button, time):
|
||||
'''
|
||||
utility function to show the popup menu
|
||||
'''
|
||||
self.emit('pre-popup')
|
||||
menu = self.get_gtkmenu(source, menu_name)
|
||||
menu.popup(None, None, None, None, button, time)
|
||||
|
||||
|
||||
class ActionGroup(object):
|
||||
'''
|
||||
container for all Actions used to associate with menu items
|
||||
'''
|
||||
|
||||
# action_state
|
||||
STANDARD = 0
|
||||
TOGGLE = 1
|
||||
|
||||
def __init__(self, shell, group_name):
|
||||
'''
|
||||
constructor
|
||||
:param shell: `RBShell`
|
||||
:param group_name: `str` unique name for the object to create
|
||||
'''
|
||||
self.group_name = group_name
|
||||
self.shell = shell
|
||||
|
||||
self._actions = {}
|
||||
|
||||
if is_rb3(self.shell):
|
||||
self.actiongroup = Gio.SimpleActionGroup()
|
||||
else:
|
||||
self.actiongroup = Gtk.ActionGroup(group_name)
|
||||
uim = self.shell.props.ui_manager
|
||||
uim.insert_action_group(self.actiongroup)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.group_name
|
||||
|
||||
def remove_actions(self):
|
||||
'''
|
||||
utility function to remove all actions associated with the ActionGroup
|
||||
'''
|
||||
for action in self.actiongroup.list_actions():
|
||||
self.actiongroup.remove_action(action)
|
||||
|
||||
def get_action(self, action_name):
|
||||
'''
|
||||
utility function to obtain the Action from the ActionGroup
|
||||
|
||||
:param action_name: `str` is the Action unique name
|
||||
'''
|
||||
return self._actions[action_name]
|
||||
|
||||
def add_action_with_accel(self, func, action_name, accel, **args):
|
||||
'''
|
||||
Creates an Action with an accelerator and adds it to the ActionGroup
|
||||
|
||||
:param func: function callback used when user activates the action
|
||||
:param action_name: `str` unique name to associate with an action
|
||||
:param accel: `str` accelerator
|
||||
:param args: dict of arguments - this is passed to the function callback
|
||||
|
||||
Notes:
|
||||
see notes for add_action
|
||||
'''
|
||||
args['accel'] = accel
|
||||
return self.add_action(func, action_name, **args)
|
||||
|
||||
def add_action(self, func, action_name, **args):
|
||||
'''
|
||||
Creates an Action and adds it to the ActionGroup
|
||||
|
||||
:param func: function callback used when user activates the action
|
||||
:param action_name: `str` unique name to associate with an action
|
||||
:param args: dict of arguments - this is passed to the function callback
|
||||
|
||||
Notes:
|
||||
key value of "label" is the visual menu label to display
|
||||
key value of "action_type" is the RB2.99 Gio.Action type ("win" or "app")
|
||||
by default it assumes all actions are "win" type
|
||||
key value of "action_state" determines what action state to create
|
||||
'''
|
||||
if 'label' in args:
|
||||
label = args['label']
|
||||
else:
|
||||
label = action_name
|
||||
|
||||
if 'accel' in args:
|
||||
accel = args['accel']
|
||||
else:
|
||||
accel = None
|
||||
|
||||
state = ActionGroup.STANDARD
|
||||
if 'action_state' in args:
|
||||
state = args['action_state']
|
||||
|
||||
if is_rb3(self.shell):
|
||||
if state == ActionGroup.TOGGLE:
|
||||
action = Gio.SimpleAction.new_stateful(action_name, None,
|
||||
GLib.Variant('b', False))
|
||||
else:
|
||||
action = Gio.SimpleAction.new(action_name, None)
|
||||
|
||||
action_type = 'win'
|
||||
if 'action_type' in args:
|
||||
if args['action_type'] == 'app':
|
||||
action_type = 'app'
|
||||
|
||||
app = Gio.Application.get_default()
|
||||
|
||||
if action_type == 'app':
|
||||
app.add_action(action)
|
||||
else:
|
||||
self.shell.props.window.add_action(action)
|
||||
self.actiongroup.add_action(action)
|
||||
|
||||
if accel:
|
||||
app.add_accelerator(accel, action_type + "." + action_name, None)
|
||||
else:
|
||||
if 'stock_id' in args:
|
||||
stock_id = args['stock_id']
|
||||
else:
|
||||
stock_id = Gtk.STOCK_CLEAR
|
||||
|
||||
if state == ActionGroup.TOGGLE:
|
||||
action = Gtk.ToggleAction(label=label,
|
||||
name=action_name,
|
||||
tooltip='', stock_id=stock_id)
|
||||
else:
|
||||
action = Gtk.Action(label=label,
|
||||
name=action_name,
|
||||
tooltip='', stock_id=stock_id)
|
||||
|
||||
if accel:
|
||||
self.actiongroup.add_action_with_accel(action, accel)
|
||||
else:
|
||||
self.actiongroup.add_action(action)
|
||||
|
||||
act = Action(self.shell, action)
|
||||
act.connect('activate', func, args)
|
||||
|
||||
act.label = label
|
||||
act.accel = accel
|
||||
|
||||
self._actions[action_name] = act
|
||||
|
||||
return act
|
||||
|
||||
|
||||
class ApplicationShell(object):
|
||||
'''
|
||||
Unique class that mirrors RB.Application & RB.Shell menu functionality
|
||||
'''
|
||||
# storage for the instance reference
|
||||
__instance = None
|
||||
|
||||
class __impl:
|
||||
""" Implementation of the singleton interface """
|
||||
|
||||
def __init__(self, shell):
|
||||
self.shell = shell
|
||||
|
||||
if is_rb3(self.shell):
|
||||
self._uids = {}
|
||||
else:
|
||||
self._uids = []
|
||||
|
||||
self._action_groups = {}
|
||||
|
||||
def insert_action_group(self, action_group):
|
||||
'''
|
||||
Adds an ActionGroup to the ApplicationShell
|
||||
|
||||
:param action_group: `ActionGroup` to add
|
||||
'''
|
||||
self._action_groups[action_group.name] = action_group
|
||||
|
||||
def lookup_action(self, action_group_name, action_name, action_type='app'):
|
||||
'''
|
||||
looks up (finds) an action created by another plugin. If found returns
|
||||
an Action or None if no matching Action.
|
||||
|
||||
:param action_group_name: `str` is the Gtk.ActionGroup name (ignored for RB2.99+)
|
||||
:param action_name: `str` unique name for the action to look for
|
||||
:param action_type: `str` RB2.99+ action type ("win" or "app")
|
||||
'''
|
||||
|
||||
if is_rb3(self.shell):
|
||||
if action_type == "app":
|
||||
action = self.shell.props.application.lookup_action(action_name)
|
||||
else:
|
||||
action = self.shell.props.window.lookup_action(action_name)
|
||||
else:
|
||||
uim = self.shell.props.ui_manager
|
||||
ui_actiongroups = uim.get_action_groups()
|
||||
|
||||
actiongroup = None
|
||||
for actiongroup in ui_actiongroups:
|
||||
if actiongroup.get_name() == action_group_name:
|
||||
break
|
||||
|
||||
action = None
|
||||
if actiongroup:
|
||||
action = actiongroup.get_action(action_name)
|
||||
|
||||
if action:
|
||||
return Action(self.shell, action)
|
||||
else:
|
||||
return None
|
||||
|
||||
def add_app_menuitems(self, ui_string, group_name, menu='tools'):
|
||||
'''
|
||||
utility function to add application menu items.
|
||||
|
||||
For RB2.99 all application menu items are added to the "tools" section of the
|
||||
application menu. All Actions are assumed to be of action_type "app".
|
||||
|
||||
For RB2.98 or less, it is added however the UI_MANAGER string
|
||||
is defined.
|
||||
|
||||
:param ui_string: `str` is the Gtk UI definition. There is not an
|
||||
equivalent UI definition in RB2.99 but we can parse out menu items since
|
||||
this string is in XML format
|
||||
|
||||
:param group_name: `str` unique name of the ActionGroup to add menu items to
|
||||
:param menu: `str` RB2.99 menu section to add to - nominally either
|
||||
'tools' or 'view'
|
||||
'''
|
||||
if is_rb3(self.shell):
|
||||
root = ET.fromstring(ui_string)
|
||||
for elem in root.findall(".//menuitem"):
|
||||
action_name = elem.attrib['action']
|
||||
item_name = elem.attrib['name']
|
||||
|
||||
group = self._action_groups[group_name]
|
||||
act = group.get_action(action_name)
|
||||
|
||||
item = Gio.MenuItem()
|
||||
item.set_detailed_action('app.' + action_name)
|
||||
item.set_label(act.label)
|
||||
item.set_attribute_value("accel", GLib.Variant("s", act.accel))
|
||||
app = Gio.Application.get_default()
|
||||
index = menu + action_name
|
||||
app.add_plugin_menu_item(menu,
|
||||
index, item)
|
||||
self._uids[index] = menu
|
||||
else:
|
||||
uim = self.shell.props.ui_manager
|
||||
self._uids.append(uim.add_ui_from_string(ui_string))
|
||||
uim.ensure_update()
|
||||
|
||||
def add_browser_menuitems(self, ui_string, group_name):
|
||||
'''
|
||||
utility function to add popup menu items to existing browser popups
|
||||
|
||||
For RB2.99 all menu items are are assumed to be of action_type "win".
|
||||
|
||||
For RB2.98 or less, it is added however the UI_MANAGER string
|
||||
is defined.
|
||||
|
||||
:param ui_string: `str` is the Gtk UI definition. There is not an
|
||||
equivalent UI definition in RB2.99 but we can parse out menu items since
|
||||
this string is in XML format
|
||||
|
||||
:param group_name: `str` unique name of the ActionGroup to add menu items to
|
||||
'''
|
||||
if is_rb3(self.shell):
|
||||
root = ET.fromstring(ui_string)
|
||||
for elem in root.findall("./popup"):
|
||||
popup_name = elem.attrib['name']
|
||||
|
||||
menuelem = elem.find('.//menuitem')
|
||||
action_name = menuelem.attrib['action']
|
||||
item_name = menuelem.attrib['name']
|
||||
|
||||
group = self._action_groups[group_name]
|
||||
act = group.get_action(action_name)
|
||||
|
||||
item = Gio.MenuItem()
|
||||
item.set_detailed_action('win.' + action_name)
|
||||
item.set_label(act.label)
|
||||
app = Gio.Application.get_default()
|
||||
|
||||
if popup_name == 'QueuePlaylistViewPopup':
|
||||
plugin_type = 'queue-popup'
|
||||
elif popup_name == 'BrowserSourceViewPopup':
|
||||
plugin_type = 'browser-popup'
|
||||
elif popup_name == 'PlaylistViewPopup':
|
||||
plugin_type = 'playlist-popup'
|
||||
elif popup_name == 'PodcastViewPopup':
|
||||
plugin_type = 'podcast-episode-popup'
|
||||
else:
|
||||
print("unknown type %s" % plugin_type)
|
||||
|
||||
index = plugin_type + action_name
|
||||
app.add_plugin_menu_item(plugin_type, index, item)
|
||||
self._uids[index] = plugin_type
|
||||
else:
|
||||
uim = self.shell.props.ui_manager
|
||||
self._uids.append(uim.add_ui_from_string(ui_string))
|
||||
uim.ensure_update()
|
||||
|
||||
def cleanup(self):
|
||||
'''
|
||||
utility remove any menuitems created.
|
||||
'''
|
||||
if is_rb3(self.shell):
|
||||
for uid in self._uids:
|
||||
Gio.Application.get_default().remove_plugin_menu_item(self._uids[uid],
|
||||
uid)
|
||||
else:
|
||||
uim = self.shell.props.ui_manager
|
||||
for uid in self._uids:
|
||||
uim.remove_ui(uid)
|
||||
uim.ensure_update();
|
||||
|
||||
def __init__(self, shell):
|
||||
""" Create singleton instance """
|
||||
# Check whether we already have an instance
|
||||
if ApplicationShell.__instance is None:
|
||||
# Create and remember instance
|
||||
ApplicationShell.__instance = ApplicationShell.__impl(shell)
|
||||
|
||||
# Store instance reference as the only member in the handle
|
||||
self.__dict__['_ApplicationShell__instance'] = ApplicationShell.__instance
|
||||
|
||||
def __getattr__(self, attr):
|
||||
""" Delegate access to implementation """
|
||||
return getattr(self.__instance, attr)
|
||||
|
||||
def __setattr__(self, attr, value):
|
||||
""" Delegate access to implementation """
|
||||
return setattr(self.__instance, attr, value)
|
||||
|
||||
|
||||
class Action(object):
|
||||
'''
|
||||
class that wraps around either a Gio.Action or a Gtk.Action
|
||||
'''
|
||||
|
||||
def __init__(self, shell, action):
|
||||
'''
|
||||
constructor.
|
||||
|
||||
:param shell: `RBShell`
|
||||
:param action: `Gio.Action` or `Gtk.Action`
|
||||
'''
|
||||
self.shell = shell
|
||||
self.action = action
|
||||
|
||||
self._label = ''
|
||||
self._accel = ''
|
||||
self._current_state = False
|
||||
self._do_update_state = True
|
||||
|
||||
def connect(self, address, func, args):
|
||||
self._connect_func = func
|
||||
self._connect_args = args
|
||||
|
||||
if address == 'activate':
|
||||
func = self._activate
|
||||
|
||||
if is_rb3(self.shell):
|
||||
self.action.connect(address, func, args)
|
||||
else:
|
||||
self.action.connect(address, func, None, args)
|
||||
|
||||
def _activate(self, action, *args):
|
||||
if self._do_update_state:
|
||||
self._current_state = not self._current_state
|
||||
self.set_state(self._current_state)
|
||||
|
||||
self._connect_func(action, None, self._connect_args)
|
||||
|
||||
@property
|
||||
def label(self):
|
||||
'''
|
||||
get the menu label associated with the Action
|
||||
|
||||
for RB2.99+ actions dont have menu labels so this is managed
|
||||
manually
|
||||
'''
|
||||
if not is_rb3(self.shell):
|
||||
return self.action.get_label()
|
||||
else:
|
||||
return self._label
|
||||
|
||||
@label.setter
|
||||
def label(self, new_label):
|
||||
if not is_rb3(self.shell):
|
||||
self.action.set_label(new_label)
|
||||
|
||||
self._label = new_label
|
||||
|
||||
@property
|
||||
def accel(self):
|
||||
'''
|
||||
get the accelerator associated with the Action
|
||||
'''
|
||||
return self._accel
|
||||
|
||||
@accel.setter
|
||||
def accel(self, new_accelerator):
|
||||
if new_accelerator:
|
||||
self._accel = new_accelerator
|
||||
else:
|
||||
self._accel = ''
|
||||
|
||||
def get_sensitive(self):
|
||||
'''
|
||||
get the sensitivity (enabled/disabled) state of the Action
|
||||
|
||||
returns boolean
|
||||
'''
|
||||
if is_rb3(self.shell):
|
||||
return self.action.get_enabled()
|
||||
else:
|
||||
return self.action.get_sensitive()
|
||||
|
||||
def set_state(self, value):
|
||||
'''
|
||||
set the state of a stateful action - this is applicable only
|
||||
to RB2.99+
|
||||
'''
|
||||
if is_rb3(self.shell) and self.action.props.state_type:
|
||||
self.action.change_state(GLib.Variant('b', value))
|
||||
|
||||
def activate(self):
|
||||
'''
|
||||
invokes the activate signal for the action
|
||||
'''
|
||||
if is_rb3(self.shell):
|
||||
self.action.activate(None)
|
||||
else:
|
||||
self.action.activate()
|
||||
|
||||
def set_active(self, value):
|
||||
'''
|
||||
activate or deactivate a stateful action signal
|
||||
For consistency with earlier RB versions, this will fire the
|
||||
activate signal for the action
|
||||
|
||||
:param value: `boolean` state value
|
||||
'''
|
||||
|
||||
if is_rb3(self.shell):
|
||||
self.action.change_state(GLib.Variant('b', value))
|
||||
self._current_state = value
|
||||
self._do_update_state = False
|
||||
self.activate()
|
||||
self._do_update_state = True
|
||||
else:
|
||||
self.action.set_active(value)
|
||||
|
||||
def get_active(self):
|
||||
'''
|
||||
get the state of the action
|
||||
|
||||
returns `boolean` state value
|
||||
'''
|
||||
if is_rb3(self.shell):
|
||||
returnval = self._current_state
|
||||
else:
|
||||
returnval = self.action.get_active()
|
||||
|
||||
return returnval
|
||||
|
||||
def associate_menuitem(self, menuitem):
|
||||
'''
|
||||
links a menu with the action
|
||||
|
||||
'''
|
||||
if is_rb3(self.shell):
|
||||
menuitem.set_detailed_action('win.' + self.action.get_name())
|
||||
else:
|
||||
menuitem.set_related_action(self.action)
|
||||
|
||||
|
||||
@@ -18,67 +18,74 @@
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
import rb
|
||||
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import WebKit
|
||||
from mako.template import Template
|
||||
|
||||
from coverart_album import AlbumManager
|
||||
import rb
|
||||
import coverart_rb3compat as rb3compat
|
||||
from coverart_album import Album
|
||||
from coverart_browser_prefs import webkit_support
|
||||
|
||||
|
||||
class CoverSearchPane(Gtk.Box):
|
||||
'''
|
||||
This UI represents a pane where different album's covers can be presented
|
||||
given an album to look for. It also allows to make custom image searchs,
|
||||
This UI represents a pane where different covers can be presented
|
||||
given an album or artist to look for. It also allows to make custom image searchs,
|
||||
customize the default search and select covers from the pane and use them
|
||||
as the album covers (either with a double click or draging them).
|
||||
as the covers (either with a double click or dragging them).
|
||||
'''
|
||||
def __init__(self, plugin, album_manager, selection_color):
|
||||
|
||||
def __init__(self, plugin, selection_color):
|
||||
'''
|
||||
Initializes the pane, loading it's html templates and it's ui.
|
||||
'''
|
||||
super(CoverSearchPane, self).__init__()
|
||||
self.set_orientation(Gtk.Orientation.VERTICAL)
|
||||
self.album_manager = album_manager
|
||||
self.selection_color = selection_color
|
||||
|
||||
self.file = ""
|
||||
self.basepath = 'file://' + plugin.plugin_info.get_data_dir()
|
||||
|
||||
self.load_templates(plugin)
|
||||
self.init_gui()
|
||||
if webkit_support():
|
||||
self.init_gui()
|
||||
|
||||
# init the pane with the empty template
|
||||
self.clear()
|
||||
# init the pane with the empty template
|
||||
self.clear()
|
||||
|
||||
def load_templates(self, plugin):
|
||||
'''
|
||||
Loads the templates and stylesheets to be used by the pane.
|
||||
'''
|
||||
# input_encoding='utf-8',
|
||||
# input_encoding='utf-8',
|
||||
|
||||
path = rb.find_plugin_file(plugin,
|
||||
'tmpl/albumartsearch-tmpl.html')
|
||||
'tmpl/albumartsearch-tmpl.html')
|
||||
self.template = Template(filename=path,
|
||||
default_filters=['decode.utf8'],
|
||||
module_directory='/tmp/',
|
||||
output_encoding='utf-8',
|
||||
encoding_errors='replace')
|
||||
default_filters=['decode.utf8'],
|
||||
module_directory='/tmp/',
|
||||
encoding_errors='replace')
|
||||
path = rb.find_plugin_file(plugin,
|
||||
'tmpl/albumartsearchempty-tmpl.html')
|
||||
'tmpl/albumartsearchempty-tmpl.html')
|
||||
self.empty_template = Template(filename=path,
|
||||
default_filters=['decode.utf8'],
|
||||
module_directory='/tmp/',
|
||||
output_encoding='utf-8',
|
||||
encoding_errors='replace')
|
||||
default_filters=['decode.utf8'],
|
||||
module_directory='/tmp/',
|
||||
encoding_errors='replace')
|
||||
path = rb.find_plugin_file(plugin,
|
||||
'tmpl/artistartsearch-tmpl.html')
|
||||
self.artist_template = Template(filename=path,
|
||||
default_filters=['decode.utf8'],
|
||||
module_directory='/tmp/',
|
||||
encoding_errors='replace')
|
||||
self.styles = rb.find_plugin_file(plugin, 'tmpl/main.css')
|
||||
|
||||
def init_gui(self):
|
||||
'''
|
||||
Initializes the pane ui.
|
||||
'''
|
||||
#---- set up webkit pane -----#
|
||||
# ---- set up webkit pane -----#
|
||||
from gi.repository import WebKit
|
||||
|
||||
self.webview = WebKit.WebView()
|
||||
settings = self.webview.get_settings()
|
||||
settings.set_property('enable-default-context-menu', False)
|
||||
@@ -91,64 +98,91 @@ class CoverSearchPane(Gtk.Box):
|
||||
self.show_all()
|
||||
|
||||
# connect the title changed signal
|
||||
#self.webview.connect('title-changed', self.set_cover)
|
||||
self.webview.connect('notify::title', self.set_cover)
|
||||
|
||||
def do_search(self, album):
|
||||
def do_search(self, coverobject, callback):
|
||||
'''
|
||||
When this method is called, the webview gets refreshed with the info
|
||||
of the album passed.
|
||||
of the album or artist passed.
|
||||
|
||||
'''
|
||||
if album is self.current_album:
|
||||
print("coverart-search do_search")
|
||||
if coverobject is self.current_searchobject:
|
||||
return
|
||||
|
||||
self.current_album = album
|
||||
self.current_searchobject = coverobject
|
||||
self.callback = callback
|
||||
|
||||
artist = album.artist
|
||||
album_name = album.name
|
||||
if isinstance(coverobject, Album):
|
||||
artist = coverobject.artist
|
||||
album_name = coverobject.name
|
||||
|
||||
if album_name.upper() == "UNKNOWN":
|
||||
album_name = ""
|
||||
if album_name.upper() == "UNKNOWN":
|
||||
album_name = ""
|
||||
|
||||
if artist.upper() == "UNKNOWN":
|
||||
artist = ""
|
||||
if artist.upper() == "UNKNOWN":
|
||||
artist = ""
|
||||
|
||||
if not (album_name == "" and artist == ""):
|
||||
artist = rb3compat.unicodestr(artist.replace('&', '&'),
|
||||
'utf-8')
|
||||
album_name = rb3compat.unicodestr(album_name.replace('&', '&'), 'utf-8')
|
||||
self.render_album_art_search(artist, album_name)
|
||||
else:
|
||||
artist_name = coverobject.name
|
||||
|
||||
if artist_name.upper() == "UNKNOWN":
|
||||
artist_name = ""
|
||||
|
||||
if not (artist_name == ""):
|
||||
artist = rb3compat.unicodestr(artist_name.replace('&', '&'),
|
||||
'utf-8')
|
||||
self.render_artist_art_search(artist)
|
||||
|
||||
if not(album_name == "" and artist == ""):
|
||||
artist = unicode(artist.replace('&', '&'),
|
||||
'utf-8')
|
||||
album_name = unicode(album_name.replace('&', '&'), 'utf-8')
|
||||
self.render_album_art_search(artist, album_name)
|
||||
|
||||
def render_album_art_search(self, artist, album_name):
|
||||
'''
|
||||
Renders the template on the webview.
|
||||
'''
|
||||
temp_file = self.template.render(artist=artist, album=album_name,
|
||||
stylesheet=self.styles, selection_color=self.selection_color)
|
||||
stylesheet=self.styles, selection_color=self.selection_color)
|
||||
|
||||
print("here")
|
||||
self.webview.load_string(temp_file, 'text/html', 'utf-8',
|
||||
self.basepath)
|
||||
self.basepath)
|
||||
|
||||
def render_artist_art_search(self, artist):
|
||||
'''
|
||||
Renders the template on the webview.
|
||||
'''
|
||||
temp_file = self.artist_template.render(artist=artist,
|
||||
stylesheet=self.styles, selection_color=self.selection_color)
|
||||
|
||||
print("here")
|
||||
self.webview.load_string(temp_file, 'text/html', 'utf-8',
|
||||
self.basepath)
|
||||
|
||||
def clear(self):
|
||||
'''
|
||||
Clears the webview of any album's specific info/covers.
|
||||
Clears the webview of any specific info/covers.
|
||||
'''
|
||||
self.current_album = None
|
||||
self.current_searchobject = None
|
||||
temp_file = self.empty_template.render(stylesheet=self.styles)
|
||||
|
||||
self.webview.load_string(temp_file, 'text/html', 'utf-8',
|
||||
self.basepath)
|
||||
self.basepath)
|
||||
|
||||
def set_cover(self, webview, arg):
|
||||
'''
|
||||
Callback called when a image in the pane is double-clicked. It takes
|
||||
care of asking the AlbumLoader to update the album's cover.
|
||||
care of updating the searched object cover.
|
||||
Some titles have spurious characters beginning with % - remove these
|
||||
'''
|
||||
# update the cover
|
||||
title = webview.get_title()
|
||||
|
||||
print title
|
||||
print(title)
|
||||
if title:
|
||||
self.album_manager.cover_man.update_cover(self.current_album,
|
||||
uri=title)
|
||||
# self.album_manager.cover_man.update_cover(self.current_searchobject,
|
||||
# uri=title)
|
||||
self.callback(self.current_searchobject, uri=title)
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
[Plugin]
|
||||
Loader=python
|
||||
Module=coverart_search_providers
|
||||
IAge=2
|
||||
Depends=rb;coverart_browser
|
||||
Name=CoverArt Browser Search Providers
|
||||
Description=Additional cover-art search providers for Rhythmbox
|
||||
Authors=fossfreedom <foss.freedom@gmail.com>, Agustín Carrasco <asermax@gmail.com>
|
||||
Copyright=© 2012 fossfreedom, Agustín Carrasco
|
||||
Website=http://github.com/fossfreedom/coverart-browser
|
||||
Help=http://github.com/fossfreedom/coverart-browser/issues
|
||||
Version=0.7
|
||||
|
||||
[RB]
|
||||
InitiallyEnabled=true
|
||||
@@ -1,103 +0,0 @@
|
||||
# -*- Mode: python; coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*-
|
||||
#
|
||||
# Copyright (C) 2012 - fossfreedom
|
||||
# Copyright (C) 2012 - Agustin Carrasco
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
# define plugin
|
||||
import rb
|
||||
import locale
|
||||
import gettext
|
||||
|
||||
from gi.repository import GObject
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import RB
|
||||
from gi.repository import GdkPixbuf
|
||||
from gi.repository import Peas
|
||||
|
||||
from coverart_browser_prefs import GSetting
|
||||
from coverart_album_search import CoverAlbumSearch
|
||||
from coverart_album_search import DiscogsSearch
|
||||
from coverart_album_search import CoverSearch
|
||||
from coverart_album_search import CoverartArchiveSearch
|
||||
|
||||
class CoverArtAlbumSearchPlugin(GObject.Object, Peas.Activatable):
|
||||
'''
|
||||
Main class of the plugin. Manages the activation and deactivation of the
|
||||
plugin.
|
||||
'''
|
||||
__gtype_name = 'CoverArtAlbumSearchPlugin'
|
||||
object = GObject.property(type=GObject.Object)
|
||||
|
||||
def __init__(self):
|
||||
'''
|
||||
Initialises the plugin object.
|
||||
'''
|
||||
GObject.Object.__init__(self)
|
||||
GObject.threads_init()
|
||||
|
||||
def do_activate(self):
|
||||
'''
|
||||
Called by Rhythmbox when the plugin is activated. It creates the
|
||||
plugin's source and connects signals to manage the plugin's
|
||||
preferences.
|
||||
'''
|
||||
|
||||
#define .plugin text strings used for translation
|
||||
#plugin = _('CoverArt Browser')
|
||||
#desc = _('Browse and play your albums through their covers')
|
||||
|
||||
print "CoverArtBrowser DEBUG - do_activate"
|
||||
self.shell = self.object
|
||||
self.db = self.shell.props.db
|
||||
|
||||
self.art_store = RB.ExtDB(name="album-art")
|
||||
self.req_id = self.art_store.connect("request", self.album_art_requested)
|
||||
|
||||
print "CoverArtBrowser DEBUG - end do_activate"
|
||||
|
||||
def do_deactivate(self):
|
||||
'''
|
||||
Called by Rhythmbox when the plugin is deactivated. It makes sure to
|
||||
free all the resources used by the plugin.
|
||||
'''
|
||||
print "CoverArtBrowser DEBUG - do_deactivate"
|
||||
|
||||
del self.shell
|
||||
del self.db
|
||||
self.art_store.disconnect(self.req_id)
|
||||
self.req_id = 0
|
||||
self.art_store = None
|
||||
|
||||
print "CoverArtBrowser DEBUG - end do_deactivate"
|
||||
|
||||
def album_art_requested(self, store, key, last_time):
|
||||
searches = []
|
||||
|
||||
gs = GSetting()
|
||||
setting = gs.get_setting(gs.Path.PLUGIN)
|
||||
|
||||
if setting[gs.PluginKey.EMBEDDED_SEARCH]:
|
||||
searches.append(CoverAlbumSearch())
|
||||
if setting[gs.PluginKey.DISCOGS_SEARCH]:
|
||||
searches.append(DiscogsSearch())
|
||||
if setting[gs.PluginKey.COVERARTARCHIVE_SEARCH]:
|
||||
searches.append(CoverartArchiveSearch())
|
||||
|
||||
print "about to search"
|
||||
s = CoverSearch(store, key, last_time, searches)
|
||||
print "finished about to return"
|
||||
return s.next_search()
|
||||
@@ -1,106 +0,0 @@
|
||||
# -*- Mode: python; coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*-
|
||||
##
|
||||
# Copyright (C) 2012 - fossfreedom
|
||||
# Copyright (C) 2012 - Agustin Carrasco
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
#
|
||||
#ttimer: Thread callback timer, it execute your callback function periodically.
|
||||
#
|
||||
#adapted from
|
||||
#
|
||||
#Author : H.K.Ong
|
||||
#Date : 27-03-2008
|
||||
#Website : http://linux.byexamples.com
|
||||
#Revision : 1
|
||||
#
|
||||
|
||||
import sys,time
|
||||
from threading import Thread
|
||||
import threading
|
||||
|
||||
|
||||
class ttimer(object):
|
||||
"""Threading callback timer - threading timer will callback your function periodically.
|
||||
interval - interval callback periodically, in sec
|
||||
retry - execute how many times? -1 is infinity
|
||||
cbfunc - callback function
|
||||
cbparam - parameter in list
|
||||
|
||||
i.e t=ttimer(1,10,myfunc,["myparam"])"""
|
||||
|
||||
def __init__(self, interval, retry, cbfunc, cbparam=[]):
|
||||
self.is_start=False
|
||||
self.is_end=False
|
||||
|
||||
# doing my thread stuff now.
|
||||
self.thread = threading.Thread(target = self._callback, args=(interval,retry,cbfunc,cbparam,) )
|
||||
self.thread.setDaemon(True)
|
||||
self.thread.start()
|
||||
#thread.start_new_thread(self._callback,(interval,retry,cbfunc,cbparam,))
|
||||
|
||||
def Start(self):
|
||||
#start the thread
|
||||
self.mytime=time.time()
|
||||
self.is_start=True
|
||||
self.is_end=False
|
||||
|
||||
def Stop(self):
|
||||
#stop the thread.
|
||||
self.mytime=time.time()
|
||||
self.is_start=False
|
||||
self.is_end=True
|
||||
|
||||
def IsStop(self):
|
||||
#Is the thread already end? return True if yes.
|
||||
if self.is_end:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def _callback(self,interval,retry,cbfunc,cbparam=[]):
|
||||
""" This is the private thread loop, call start() to start the threading timer."""
|
||||
print "callback"
|
||||
self.retry=retry
|
||||
retry=0
|
||||
|
||||
if self.is_end:
|
||||
return None
|
||||
|
||||
while True:
|
||||
if not self.is_end:
|
||||
if self.retry==-1:
|
||||
pass
|
||||
elif retry>=self.retry:
|
||||
break
|
||||
|
||||
if self.is_start:
|
||||
#check time
|
||||
tmptime=time.time()
|
||||
if tmptime >=(self.mytime + interval):
|
||||
print "before"
|
||||
cbfunc(cbparam) # callback your function
|
||||
print "after"
|
||||
self.mytime=time.time()
|
||||
|
||||
if not self.retry== -1:
|
||||
retry+=1
|
||||
else:
|
||||
pass
|
||||
time.sleep(0.5)
|
||||
|
||||
self.is_end=True
|
||||
print "end callback"
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
# -*- Mode: python; coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*-
|
||||
#
|
||||
# Copyright (C) 2012 - fossfreedom
|
||||
# Copyright (C) 2012 - Agustin Carrasco
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
from gi.repository import GObject
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import RB
|
||||
from gi.repository import Gio
|
||||
|
||||
from coverart_browser_prefs import GSetting
|
||||
from coverart_browser_prefs import CoverLocale
|
||||
from coverart_utils import Theme
|
||||
from coverart_controllers import PlaylistPopupController
|
||||
from coverart_controllers import GenrePopupController
|
||||
from coverart_controllers import SortPopupController
|
||||
from coverart_controllers import ArtistSortPopupController
|
||||
from coverart_controllers import PropertiesMenuController
|
||||
from coverart_controllers import DecadePopupController
|
||||
from coverart_controllers import SortOrderToggleController
|
||||
from coverart_controllers import ArtistSortOrderToggleController
|
||||
from coverart_controllers import AlbumSearchEntryController
|
||||
from coverart_widgets import SearchEntry
|
||||
from coverart_browser_prefs import webkit_support
|
||||
import rb
|
||||
|
||||
|
||||
class Toolbar(GObject.Object):
|
||||
def __init__(self, plugin, mainbox, controllers):
|
||||
super(Toolbar, self).__init__()
|
||||
|
||||
self.plugin = plugin
|
||||
self.mainbox = mainbox
|
||||
cl = CoverLocale()
|
||||
|
||||
ui_file = rb.find_plugin_file(plugin, self.ui)
|
||||
|
||||
# create the toolbar
|
||||
builder = Gtk.Builder()
|
||||
builder.set_translation_domain(cl.Locale.LOCALE_DOMAIN)
|
||||
print (ui_file)
|
||||
builder.add_from_file(ui_file)
|
||||
|
||||
# assign the controllers to the buttons
|
||||
for button, controller in controllers.items():
|
||||
if button != 'search':
|
||||
builder.get_object(button).controller = controller
|
||||
|
||||
if not webkit_support():
|
||||
# button = builder.get_object('flowview_button')
|
||||
#button.set_visible(False)
|
||||
separator = builder.get_object('properties_separator')
|
||||
if separator:
|
||||
separator.set_visible(False)
|
||||
|
||||
# workaround to translate the search entry tooltips
|
||||
cl.switch_locale(cl.Locale.RB)
|
||||
search_entry = SearchEntry(has_popup=True)
|
||||
search_entry.show_all()
|
||||
cl.switch_locale(cl.Locale.LOCALE_DOMAIN)
|
||||
|
||||
# add it to the ui
|
||||
align = builder.get_object('entry_search_alignment')
|
||||
align.add(search_entry)
|
||||
|
||||
# assign the controller
|
||||
search_entry.controller = controllers['search']
|
||||
|
||||
Theme(self.plugin).connect('theme_changed', self._theme_changed,
|
||||
controllers)
|
||||
|
||||
self.builder = builder.get_object('toolbar')
|
||||
|
||||
# now theme the toolbar including child objects such as the button popups
|
||||
style_context = self.builder.get_style_context()
|
||||
style_context.add_class(Gtk.STYLE_CLASS_TOOLBAR)
|
||||
|
||||
view_button = builder.get_object(ToolbarObject.VIEW)
|
||||
view_button.set_visible(not self.plugin.using_headerbar)
|
||||
|
||||
def _theme_changed(self, toolbar, controllers):
|
||||
for controller in list(controllers.values()):
|
||||
controller.update_images(True)
|
||||
|
||||
|
||||
class TopToolbar(Toolbar):
|
||||
ui = 'ui/coverart_topbar.ui'
|
||||
name = 'main'
|
||||
|
||||
def hide(self):
|
||||
if self.builder.get_visible():
|
||||
self.builder.hide()
|
||||
|
||||
def show(self):
|
||||
self.mainbox.pack_start(self.builder, False, True, 0)
|
||||
self.mainbox.reorder_child(self.builder, 0)
|
||||
self.builder.show()
|
||||
|
||||
|
||||
class LeftToolbar(Toolbar):
|
||||
ui = 'ui/coverart_leftsidebar.ui'
|
||||
name = 'left'
|
||||
|
||||
def hide(self):
|
||||
if self.builder.get_visible():
|
||||
self.builder.hide()
|
||||
self.plugin.shell.remove_widget(self.builder,
|
||||
RB.ShellUILocation.SIDEBAR)
|
||||
|
||||
def show(self):
|
||||
self.plugin.shell.add_widget(self.builder,
|
||||
RB.ShellUILocation.SIDEBAR, expand=False, fill=False)
|
||||
self.builder.show()
|
||||
|
||||
|
||||
class RightToolbar(Toolbar):
|
||||
ui = 'ui/coverart_rightsidebar.ui'
|
||||
name = 'right'
|
||||
|
||||
def hide(self):
|
||||
if self.builder.get_visible():
|
||||
self.builder.hide()
|
||||
self.plugin.shell.remove_widget(self.builder,
|
||||
RB.ShellUILocation.RIGHT_SIDEBAR)
|
||||
|
||||
def show(self):
|
||||
self.plugin.shell.add_widget(self.builder,
|
||||
RB.ShellUILocation.RIGHT_SIDEBAR, expand=False, fill=False)
|
||||
self.builder.show()
|
||||
|
||||
|
||||
class ToolbarObject(object):
|
||||
# properties
|
||||
|
||||
PROPERTIES = 'properties_button'
|
||||
SORT_BY = 'sort_by'
|
||||
SORT_ORDER = 'sort_order'
|
||||
SORT_BY_ARTIST = 'sort_by_artist'
|
||||
SORT_ORDER_ARTIST = 'sort_order_artist'
|
||||
GENRE = 'genre_button'
|
||||
PLAYLIST = 'playlist_button'
|
||||
DECADE = 'decade_button'
|
||||
SEARCH = 'search'
|
||||
VIEW = 'view_button'
|
||||
|
||||
|
||||
class ToolbarManager(GObject.Object):
|
||||
# properties
|
||||
toolbar_pos = GObject.property(type=str, default=TopToolbar.name)
|
||||
|
||||
def __init__(self, plugin, main_box, viewmgr):
|
||||
super(ToolbarManager, self).__init__()
|
||||
self.plugin = plugin
|
||||
# create the buttons controllers
|
||||
controllers = self._create_controllers(plugin, viewmgr)
|
||||
|
||||
# initialize toolbars
|
||||
self._bars = {}
|
||||
self._bars[TopToolbar.name] = TopToolbar(plugin, main_box,
|
||||
controllers)
|
||||
self._bars[LeftToolbar.name] = LeftToolbar(plugin, main_box,
|
||||
controllers)
|
||||
self._bars[RightToolbar.name] = RightToolbar(plugin, main_box,
|
||||
controllers)
|
||||
|
||||
self.last_toolbar_pos = None
|
||||
|
||||
# if the alternative-toolbar is loaded then lets connect to the toolbar-visibility signal
|
||||
# to control our sources toolbar visibility
|
||||
|
||||
if self.plugin.using_alternative_toolbar:
|
||||
if self.plugin.using_headerbar:
|
||||
self.toolbar_pos = TopToolbar.name # we dont allow other toolbar position with headerbar
|
||||
self._on_notify_toolbar_pos()
|
||||
|
||||
self.plugin.shell.alternative_toolbar.connect('toolbar-visibility', self._visibility)
|
||||
|
||||
# connect signal and properties
|
||||
self._connect_signals()
|
||||
self._connect_properties()
|
||||
|
||||
self._controllers = controllers
|
||||
|
||||
def _visibility(self, altplugin, value):
|
||||
if value:
|
||||
self._bars[self.toolbar_pos].show()
|
||||
else:
|
||||
self._bars[self.toolbar_pos].hide()
|
||||
|
||||
def set_enabled(self, enabled, toolbar_object=None):
|
||||
'''
|
||||
enable or disable the toolbar object.
|
||||
|
||||
:param enabled: `bool` value.
|
||||
:param toolbar_object: `ToolbarObject`
|
||||
None if enabled is to apply to all objects in the toolbar
|
||||
|
||||
'''
|
||||
if toolbar_object:
|
||||
self._controllers[toolbar_object].enabled = enabled
|
||||
else:
|
||||
for controller in self._controllers:
|
||||
self._controllers[controller].enabled = enabled
|
||||
|
||||
def _connect_signals(self):
|
||||
if not self.plugin.using_headerbar:
|
||||
self.connect('notify::toolbar-pos', self._on_notify_toolbar_pos)
|
||||
|
||||
def _connect_properties(self):
|
||||
if not self.plugin.using_headerbar:
|
||||
gs = GSetting()
|
||||
setting = gs.get_setting(gs.Path.PLUGIN)
|
||||
setting.bind(gs.PluginKey.TOOLBAR_POS, self, 'toolbar_pos',
|
||||
Gio.SettingsBindFlags.GET)
|
||||
|
||||
def _create_controllers(self, plugin, viewmgr):
|
||||
controllers = {}
|
||||
|
||||
album_model = viewmgr.source.album_manager.model
|
||||
controllers[ToolbarObject.PROPERTIES] = \
|
||||
PropertiesMenuController(plugin, viewmgr.source)
|
||||
controllers[ToolbarObject.SORT_BY] = \
|
||||
SortPopupController(plugin, viewmgr)
|
||||
controllers[ToolbarObject.SORT_ORDER] = \
|
||||
SortOrderToggleController(plugin, viewmgr)
|
||||
controllers[ToolbarObject.SORT_BY_ARTIST] = \
|
||||
ArtistSortPopupController(plugin, viewmgr)
|
||||
controllers[ToolbarObject.SORT_ORDER_ARTIST] = \
|
||||
ArtistSortOrderToggleController(plugin, viewmgr)
|
||||
controllers[ToolbarObject.GENRE] = \
|
||||
GenrePopupController(plugin, album_model)
|
||||
controllers[ToolbarObject.PLAYLIST] = \
|
||||
PlaylistPopupController(plugin, album_model)
|
||||
controllers[ToolbarObject.DECADE] = \
|
||||
DecadePopupController(plugin, album_model)
|
||||
controllers[ToolbarObject.SEARCH] = \
|
||||
AlbumSearchEntryController(album_model)
|
||||
|
||||
controllers[ToolbarObject.VIEW] = viewmgr.controller
|
||||
|
||||
return controllers
|
||||
|
||||
def _on_notify_toolbar_pos(self, *args):
|
||||
if self.last_toolbar_pos:
|
||||
self._bars[self.last_toolbar_pos].hide()
|
||||
|
||||
self._bars[self.toolbar_pos].show()
|
||||
|
||||
self.last_toolbar_pos = self.toolbar_pos
|
||||
@@ -15,16 +15,85 @@
|
||||
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
from bisect import bisect_left, bisect_right
|
||||
import collections
|
||||
import re
|
||||
import logging
|
||||
import sys
|
||||
from collections import namedtuple
|
||||
|
||||
from gi.repository import GdkPixbuf
|
||||
from gi.repository import Gdk
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import GLib
|
||||
from gi.repository import RB
|
||||
from gi.repository import GObject
|
||||
from gi.repository import Gio
|
||||
import lxml.etree as ET
|
||||
|
||||
import rb
|
||||
from coverart_browser_prefs import CoverLocale
|
||||
import collections
|
||||
import re
|
||||
from coverart_browser_prefs import GSetting
|
||||
import coverart_rb3compat as rb3compat
|
||||
from coverart_search_providers import lastfm_connected
|
||||
from coverart_search_providers import get_search_providers
|
||||
|
||||
|
||||
class FauxTb(object):
|
||||
def __init__(self, tb_frame, tb_lineno, tb_next):
|
||||
self.tb_frame = tb_frame
|
||||
self.tb_lineno = tb_lineno
|
||||
self.tb_next = tb_next
|
||||
|
||||
|
||||
def current_stack(skip=0):
|
||||
try:
|
||||
1 / 0
|
||||
except ZeroDivisionError:
|
||||
f = sys.exc_info()[2].tb_frame
|
||||
for i in range(skip + 2):
|
||||
f = f.f_back
|
||||
lst = []
|
||||
while f is not None:
|
||||
lst.append((f, f.f_lineno))
|
||||
f = f.f_back
|
||||
return lst
|
||||
|
||||
|
||||
def extend_traceback(tb, stack):
|
||||
"""Extend traceback with stack info."""
|
||||
head = tb
|
||||
for tb_frame, tb_lineno in stack:
|
||||
head = FauxTb(tb_frame, tb_lineno, head)
|
||||
return head
|
||||
|
||||
|
||||
def full_exc_info():
|
||||
"""Like sys.exc_info, but includes the full traceback."""
|
||||
t, v, tb = sys.exc_info()
|
||||
full_tb = extend_traceback(tb, current_stack(1))
|
||||
return t, v, full_tb
|
||||
|
||||
|
||||
def dumpstack(message):
|
||||
''' dumps the current stack - useful of debugging
|
||||
'''
|
||||
logging.error(message, exc_info=full_exc_info())
|
||||
|
||||
|
||||
def uniquify_and_sort(iterable):
|
||||
''' Removes duplicates of an iterables and returns a list of unique
|
||||
elements.
|
||||
'''
|
||||
uniques = []
|
||||
|
||||
for element in iterable:
|
||||
if element not in uniques:
|
||||
uniques.append(element)
|
||||
|
||||
return sorted(uniques)
|
||||
|
||||
|
||||
GenreType = namedtuple("GenreType", ["name", "genre_type"])
|
||||
|
||||
|
||||
class NaturalString(str):
|
||||
@@ -35,10 +104,10 @@ class NaturalString(str):
|
||||
'''
|
||||
|
||||
def __init__(self, string):
|
||||
super(NaturalString, self).__init__(string)
|
||||
super(NaturalString, self).__init__()
|
||||
convert = lambda text: int(text) if text.isdigit() else text.lower()
|
||||
alphanum_key = lambda key: [convert(c) for c in re.split('([0-9]+)',
|
||||
key)]
|
||||
key)]
|
||||
|
||||
self._string_elements = alphanum_key(string)
|
||||
|
||||
@@ -239,7 +308,6 @@ class SortedCollection(object):
|
||||
|
||||
|
||||
class ReversedSortedCollection(object):
|
||||
|
||||
def __init__(self, sorted_collection):
|
||||
self._sorted_collection = sorted_collection
|
||||
|
||||
@@ -295,7 +363,6 @@ class ReversedSortedCollection(object):
|
||||
|
||||
|
||||
class IdleCallIterator(object):
|
||||
|
||||
def __init__(self, chunk, process, after=None, error=None, finish=None):
|
||||
default = lambda *_: None
|
||||
|
||||
@@ -309,7 +376,8 @@ class IdleCallIterator(object):
|
||||
def __call__(self, iterator, **data):
|
||||
self._iter = iterator
|
||||
|
||||
Gdk.threads_add_idle(GLib.PRIORITY_DEFAULT_IDLE, self._idle_call, data)
|
||||
#Gdk.threads_add_idle(GLib.PRIORITY_DEFAULT_IDLE, self._idle_call, data)
|
||||
GLib.idle_add(self._idle_call, data)
|
||||
|
||||
def _idle_call(self, data):
|
||||
if self._stop:
|
||||
@@ -317,7 +385,7 @@ class IdleCallIterator(object):
|
||||
|
||||
for i in range(self._chunk):
|
||||
try:
|
||||
next_elem = self._iter.next()
|
||||
next_elem = next(self._iter)
|
||||
|
||||
self._process(next_elem, data)
|
||||
except StopIteration:
|
||||
@@ -345,10 +413,88 @@ def idle_iterator(func):
|
||||
return iter_function
|
||||
|
||||
|
||||
class SpriteSheet(object):
|
||||
class Theme:
|
||||
'''
|
||||
This class manages the theme details
|
||||
'''
|
||||
# storage for the instance reference
|
||||
__instance = None
|
||||
|
||||
class _impl(GObject.Object):
|
||||
""" Implementation of the singleton interface """
|
||||
# properties
|
||||
theme = GObject.property(type=str, default="standard")
|
||||
|
||||
# signals
|
||||
'''
|
||||
changed = signal emitted when a theme has changed
|
||||
'''
|
||||
__gsignals__ = {
|
||||
'theme_changed': (GObject.SIGNAL_RUN_LAST, None, ())
|
||||
}
|
||||
# below public variables and methods that can be called for Theme
|
||||
def __init__(self, plugin):
|
||||
'''
|
||||
Initializes the singleton interface, assigning all the constants
|
||||
used to access the plugin's settings.
|
||||
'''
|
||||
super(Theme._impl, self).__init__()
|
||||
|
||||
self.plugin = plugin
|
||||
popups = rb.find_plugin_file(plugin, 'img/popups.xml')
|
||||
root = ET.parse(open(popups)).getroot()
|
||||
|
||||
base = 'theme/theme'
|
||||
self.themes = []
|
||||
|
||||
for elem in root.xpath(base):
|
||||
self.themes.append(elem.attrib['folder_name'])
|
||||
|
||||
self.gs = GSetting()
|
||||
self.setting = self.gs.get_setting(self.gs.Path.PLUGIN)
|
||||
|
||||
# connect properties and signals
|
||||
self._connect_properties()
|
||||
self._connect_signals()
|
||||
|
||||
@property
|
||||
def current(self):
|
||||
return self.setting[self.gs.PluginKey.THEME]
|
||||
|
||||
def _connect_properties(self):
|
||||
self.setting.bind(self.gs.PluginKey.THEME, self,
|
||||
'theme', Gio.SettingsBindFlags.GET)
|
||||
|
||||
def _connect_signals(self):
|
||||
self.connect('notify::theme', self._on_theme_changed,
|
||||
None)
|
||||
|
||||
def _on_theme_changed(self, *args):
|
||||
self.emit('theme_changed')
|
||||
|
||||
def __init__(self, plugin):
|
||||
""" Create singleton instance """
|
||||
# Check whether we already have an instance
|
||||
if Theme.__instance is None:
|
||||
# Create and remember instance
|
||||
Theme.__instance = Theme._impl(plugin)
|
||||
|
||||
# Store instance reference as the only member in the handle
|
||||
self.__dict__['_Theme__instance'] = Theme.__instance
|
||||
|
||||
def __getattr__(self, attr):
|
||||
""" Delegate access to implementation """
|
||||
return getattr(self.__instance, attr)
|
||||
|
||||
def __setattr__(self, attr, value):
|
||||
""" Delegate access to implementation """
|
||||
return setattr(self.__instance, attr, value)
|
||||
|
||||
|
||||
class SpriteSheet(object):
|
||||
def __init__(self, image, icon_width, icon_height, x_spacing, y_spacing,
|
||||
x_start, y_start, alpha_color=None, size=None):
|
||||
x_start, y_start, across_dimension, down_dimension,
|
||||
alpha_color=None, size=None):
|
||||
# load the image
|
||||
base_image = GdkPixbuf.Pixbuf.new_from_file(image)
|
||||
|
||||
@@ -360,19 +506,18 @@ class SpriteSheet(object):
|
||||
|
||||
self._sprites = []
|
||||
|
||||
for y in range(0, ((base_image.get_height() - y_start) / delta_y) + 1):
|
||||
for x in range(0, ((base_image.get_width() - x_start) / delta_x)
|
||||
+ 1):
|
||||
for y in range(0, down_dimension):
|
||||
for x in range(0, across_dimension):
|
||||
sprite = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, True,
|
||||
8, icon_width, icon_height)
|
||||
8, icon_width, icon_height)
|
||||
|
||||
base_image.copy_area(x_start + (x * delta_x),
|
||||
y_start + (y * delta_y), icon_width, icon_height,
|
||||
sprite, 0, 0)
|
||||
y_start + (y * delta_y), icon_width, icon_height,
|
||||
sprite, 0, 0)
|
||||
|
||||
if size:
|
||||
sprite = sprite.scale_simple(size[0], size[1],
|
||||
GdkPixbuf.InterpType.BILINEAR)
|
||||
GdkPixbuf.InterpType.BILINEAR)
|
||||
|
||||
self._sprites.append(sprite)
|
||||
|
||||
@@ -385,22 +530,24 @@ class SpriteSheet(object):
|
||||
|
||||
class ConfiguredSpriteSheet(object):
|
||||
def __init__(self, plugin, sprite_name, size=None):
|
||||
self.plugin = plugin
|
||||
popups = rb.find_plugin_file(plugin, 'img/popups.xml')
|
||||
self.root = ET.parse(open(popups)).getroot()
|
||||
base = 'spritesheet[@name="' + sprite_name + '"]/'
|
||||
image = rb.find_plugin_file(plugin, 'img/' +
|
||||
self.root.xpath(base + 'image')[0].text)
|
||||
icon_width = int(self.root.xpath(base + 'icon')[0].attrib['width'])
|
||||
icon_height = int(self.root.xpath(base + 'icon')[0].attrib['height'])
|
||||
x_spacing = int(self.root.xpath(base + 'spacing')[0].attrib['x'])
|
||||
y_spacing = int(self.root.xpath(base + 'spacing')[0].attrib['y'])
|
||||
x_start = int(self.root.xpath(base + 'start-position')[0].attrib['x'])
|
||||
y_start = int(self.root.xpath(base + 'start-position')[0].attrib['y'])
|
||||
root = ET.parse(open(popups)).getroot()
|
||||
base = 'theme/theme[@folder_name="' + Theme(plugin).current \
|
||||
+ '"]/spritesheet[@name="' + sprite_name + '"]/'
|
||||
image = rb.find_plugin_file(plugin, 'img/' + Theme(plugin).current \
|
||||
+ '/' + root.xpath(base + 'image')[0].text)
|
||||
icon_width = int(root.xpath(base + 'icon')[0].attrib['width'])
|
||||
icon_height = int(root.xpath(base + 'icon')[0].attrib['height'])
|
||||
x_spacing = int(root.xpath(base + 'spacing')[0].attrib['x'])
|
||||
y_spacing = int(root.xpath(base + 'spacing')[0].attrib['y'])
|
||||
x_start = int(root.xpath(base + 'start-position')[0].attrib['x'])
|
||||
y_start = int(root.xpath(base + 'start-position')[0].attrib['y'])
|
||||
across_dimension = int(root.xpath(base + 'dimension')[0].attrib['across'])
|
||||
down_dimension = int(root.xpath(base + 'dimension')[0].attrib['down'])
|
||||
|
||||
try:
|
||||
alpha_color = map(int,
|
||||
self.root.xpath(base + 'alpha')[0].text.split(' '))
|
||||
alpha_color = list(map(int,
|
||||
root.xpath(base + 'alpha')[0].text.split(' ')))
|
||||
except:
|
||||
alpha_color = None
|
||||
|
||||
@@ -408,24 +555,27 @@ class ConfiguredSpriteSheet(object):
|
||||
self.locale_names = {}
|
||||
|
||||
cl = CoverLocale()
|
||||
lang=cl.get_locale()
|
||||
lang = cl.get_locale()
|
||||
|
||||
base = sprite_name + '/' + sprite_name +\
|
||||
'[@spritesheet="' + sprite_name + '"]'
|
||||
base = sprite_name + '/' + sprite_name + \
|
||||
'[@spritesheet="' + sprite_name + '"]'
|
||||
|
||||
for elem in self.root.xpath(base + '[not(@xml:lang)]'):
|
||||
for elem in root.xpath(base + '[not(@xml:lang)]'):
|
||||
self.names.append(elem.text)
|
||||
|
||||
for elem in self.root.xpath(base + '[@xml:lang="' + lang + '"]'):
|
||||
self.locale_names[elem.text]=elem.attrib['name']
|
||||
for elem in root.xpath(base + '[@xml:lang="' + lang + '"]'):
|
||||
self.locale_names[elem.text] = elem.attrib['name']
|
||||
|
||||
if (not self.locale_names) and len(lang) > 2:
|
||||
for elem in self.root.xpath(base + '[@xml:lang="' +\
|
||||
lang[0:2] + '"]'):
|
||||
self.locale_names[elem.text]=elem.attrib['name']
|
||||
for elem in root.xpath(base + '[@xml:lang="' + \
|
||||
lang[0:2] + '"]'):
|
||||
self.locale_names[elem.text] = elem.attrib['name']
|
||||
|
||||
self._sheet = SpriteSheet(image, icon_width, icon_height, x_spacing,
|
||||
y_spacing, x_start, y_start, alpha_color, size)
|
||||
y_spacing, x_start, y_start, across_dimension, down_dimension,
|
||||
alpha_color, size)
|
||||
|
||||
self._genre_db = RB.ExtDB(name='cb_genre')
|
||||
|
||||
def __len__(self):
|
||||
return len(self._sheet)
|
||||
@@ -439,28 +589,159 @@ class ConfiguredSpriteSheet(object):
|
||||
def __contains__(self, name):
|
||||
return name in self.names
|
||||
|
||||
def keys(self):
|
||||
return self.names
|
||||
|
||||
|
||||
class GenreConfiguredSpriteSheet(ConfiguredSpriteSheet):
|
||||
'''
|
||||
A sprite-sheet of genres. Creates a pixbuf representation of a picture
|
||||
that has several icons in a regular pattern. This uses the file
|
||||
'popups.xml' for its definition
|
||||
|
||||
:plugin: rhythmbox plugin
|
||||
:sprite_name: `str` containing name of the spritesheet pattern in
|
||||
popups.xml
|
||||
:size: `int` array dimension of the final sprite which is to be used.
|
||||
|
||||
output:
|
||||
:names: `str` array of sprite names
|
||||
'''
|
||||
# types of genre
|
||||
GENRE_USER = 1
|
||||
GENRE_SYSTEM = 2
|
||||
GENRE_LOCALE = 3
|
||||
|
||||
def __init__(self, plugin, sprite_name, size=None):
|
||||
super(GenreConfiguredSpriteSheet, self).__init__(plugin, sprite_name,
|
||||
size)
|
||||
self.alternate = {}
|
||||
self.locale_alternate = {}
|
||||
size)
|
||||
self.genre_alternate = {} # contains GenreType tuples
|
||||
self._alt_icons = {}
|
||||
self._sprite_name = sprite_name
|
||||
self._size = size
|
||||
|
||||
popups = rb.find_plugin_file(plugin, 'img/popups.xml')
|
||||
root = ET.parse(open(popups)).getroot()
|
||||
self._parse_popups(plugin, root, self.GENRE_SYSTEM)
|
||||
|
||||
try:
|
||||
# self._user_popups = RB.find_user_data_file('plugins/coverart_browser/img/usericons/popups.xml')
|
||||
self._user_popups = RB.user_cache_dir() + "/coverart_browser/usericons/popups.xml"
|
||||
root = ET.parse(open(self._user_popups)).getroot()
|
||||
self._parse_popups(plugin, root, self.GENRE_USER)
|
||||
elem = root.xpath(self._sprite_name + '/index')
|
||||
curr_index = int(elem[0].text)
|
||||
|
||||
for index in range(0, curr_index + 1):
|
||||
key = RB.ExtDBKey.create_lookup('icon', str(index))
|
||||
icon_location = self._genre_db.lookup(key)
|
||||
sprite = GdkPixbuf.Pixbuf.new_from_file(icon_location)
|
||||
if self._size:
|
||||
sprite = sprite.scale_simple(self._size[0], self._size[1],
|
||||
GdkPixbuf.InterpType.BILINEAR)
|
||||
|
||||
self._alt_icons[str(index)] = sprite
|
||||
self.names.append(str(index))
|
||||
except:
|
||||
pass
|
||||
|
||||
def __getitem__(self, name):
|
||||
try:
|
||||
return self._alt_icons[name]
|
||||
except:
|
||||
return self._sheet[self.names.index(name)]
|
||||
|
||||
def _parse_popups(self, plugin, root, genre_type):
|
||||
icon_names = {}
|
||||
cl = CoverLocale()
|
||||
lang=cl.get_locale()
|
||||
lang = cl.get_locale()
|
||||
|
||||
base = sprite_name + '/alt'
|
||||
for elem in self.root.xpath(base + '[not(@xml:lang)]/alt'):
|
||||
self.alternate[elem.text] = elem.attrib['genre']
|
||||
base = self._sprite_name + '/alt'
|
||||
for elem in root.xpath(base + '[not(@xml:lang)]/alt'):
|
||||
self.genre_alternate[GenreType(name=elem.text, genre_type=genre_type)] = elem.attrib['genre']
|
||||
|
||||
for elem in self.root.xpath(base + '[@xml:lang="' + lang + '"]/alt'):
|
||||
self.locale_alternate[elem.text] = elem.attrib['genre']
|
||||
for elem in root.xpath(base + '[@xml:lang="' + lang + '"]/alt'):
|
||||
self.genre_alternate[GenreType(name=elem.text, genre_type=self.GENRE_LOCALE)] = elem.attrib['genre']
|
||||
|
||||
if (not self.locale_alternate) and len(lang) > 2:
|
||||
for elem in self.root.xpath(base + '[@xml:lang="' +\
|
||||
lang[0:2] + '"]/alt'):
|
||||
self.locale_alternate[elem.text] = elem.attrib['genre']
|
||||
# if (not self.locale_alternate) and len(lang) > 2:
|
||||
if len(lang) > 2:
|
||||
for elem in root.xpath(base + '[@xml:lang="' + \
|
||||
lang[0:2] + '"]/alt'):
|
||||
self.genre_alternate[GenreType(name=elem.text, genre_type=self.GENRE_LOCALE)] = elem.attrib['genre']
|
||||
|
||||
def add_genre_icon(self, filename):
|
||||
root = ET.parse(open(self._user_popups)).getroot()
|
||||
elem = root.xpath(self._sprite_name + '/index')
|
||||
next_index = int(elem[0].text)
|
||||
elem[0].text = str(next_index + 1)
|
||||
tree = ET.ElementTree(root)
|
||||
tree.write(self._user_popups, pretty_print=True, xml_declaration=True)
|
||||
|
||||
key = RB.ExtDBKey.create_storage('icon', str(next_index))
|
||||
uri = "file://" + rb3compat.pathname2url(filename)
|
||||
|
||||
self._genre_db.store_uri(key, RB.ExtDBSourceType.USER_EXPLICIT, uri)
|
||||
|
||||
pixbuf = GdkPixbuf.Pixbuf.new_from_file(filename)
|
||||
new_genre = GenreType(name=str(next_index), genre_type=self.GENRE_USER)
|
||||
|
||||
if self._size:
|
||||
pixbuf = pixbuf.scale_simple(self._size[0], self._size[1],
|
||||
GdkPixbuf.InterpType.BILINEAR)
|
||||
|
||||
self._alt_icons[new_genre.name] = pixbuf
|
||||
self.names.append(new_genre.name)
|
||||
|
||||
return new_genre
|
||||
|
||||
def delete_genre(self, current_genre):
|
||||
root = ET.parse(open(self._user_popups)).getroot()
|
||||
base = self._sprite_name + '/alt/alt'
|
||||
|
||||
found = False
|
||||
|
||||
for elem in root.xpath(base):
|
||||
if RB.search_fold(elem.text) == RB.search_fold(current_genre):
|
||||
found = True
|
||||
break
|
||||
|
||||
if found:
|
||||
elem.getparent().remove(elem)
|
||||
tree = ET.ElementTree(root)
|
||||
tree.write(self._user_popups, pretty_print=True, xml_declaration=True)
|
||||
else:
|
||||
print("not found to delete")
|
||||
|
||||
|
||||
def amend_genre_info(self, current_genre, new_genre, icon_name):
|
||||
root = ET.parse(open(self._user_popups)).getroot()
|
||||
base = self._sprite_name + '/alt/alt'
|
||||
|
||||
found = False
|
||||
|
||||
if current_genre != "":
|
||||
for elem in root.xpath(base):
|
||||
if RB.search_fold(elem.text) == RB.search_fold(current_genre):
|
||||
found = True
|
||||
del self.genre_alternate[GenreType(name=elem.text, genre_type=self.GENRE_USER)]
|
||||
break
|
||||
|
||||
else:
|
||||
elem = ET.SubElement(root.xpath(self._sprite_name + '/alt')[0], "alt")
|
||||
if elem != None:
|
||||
found = True
|
||||
|
||||
if found:
|
||||
elem.text = rb3compat.unicodestr(new_genre, 'utf-8')
|
||||
elem.attrib['genre'] = icon_name
|
||||
|
||||
tree = ET.ElementTree(root)
|
||||
tree.write(self._user_popups, pretty_print=True, xml_declaration=True)
|
||||
self.genre_alternate[GenreType(name=elem.text, genre_type=self.GENRE_USER)] = icon_name
|
||||
return GenreType(name=elem.text, genre_type=self.GENRE_USER)
|
||||
else:
|
||||
print("nothing found to amend")
|
||||
return None
|
||||
|
||||
|
||||
def get_stock_size():
|
||||
@@ -474,26 +755,84 @@ def create_pixbuf_from_file_at_size(filename, width, height):
|
||||
|
||||
if pixbuf.get_width() != width or pixbuf.get_height() != height:
|
||||
pixbuf = pixbuf.scale_simple(width, height,
|
||||
GdkPixbuf.InterpType.BILINEAR)
|
||||
GdkPixbuf.InterpType.BILINEAR)
|
||||
|
||||
return pixbuf
|
||||
|
||||
|
||||
'''
|
||||
class to search through a dict without case-sensitivity nor
|
||||
unicode vs string issues
|
||||
'''
|
||||
|
||||
|
||||
class CaseInsensitiveDict(collections.Mapping):
|
||||
def __init__(self, d):
|
||||
self._d = d
|
||||
self._s = dict((RB.search_fold(k), k) for k in d)
|
||||
|
||||
def __contains__(self, k):
|
||||
return RB.search_fold(k) in self._s
|
||||
|
||||
def __len__(self):
|
||||
return len(self._s)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._s)
|
||||
|
||||
def __getitem__(self, k):
|
||||
return self._d[self._s[RB.search_fold(k)]]
|
||||
|
||||
def actual_key_case(self, k):
|
||||
return self._s.get(RB.search_fold(k))
|
||||
|
||||
|
||||
def check_lastfm(force_check=False):
|
||||
'''
|
||||
check validity of lastfm connection
|
||||
|
||||
returns True if connected with an account
|
||||
|
||||
Also returns True if lastFM is not in the list of search providers
|
||||
'''
|
||||
|
||||
providers = get_search_providers()
|
||||
print(providers)
|
||||
print(force_check)
|
||||
|
||||
if force_check or 'lastfm-search' in providers:
|
||||
connected = lastfm_connected()
|
||||
print(connected)
|
||||
return connected
|
||||
elif not 'lastfm-search' in providers:
|
||||
print("not lastm-search")
|
||||
return True
|
||||
else:
|
||||
print("returning default")
|
||||
return False
|
||||
|
||||
def create_button_image_symbolic(style_context, icon_name):
|
||||
'''
|
||||
create a pixbuf for the given symbolic icon_name sized according to the stock icon size
|
||||
'''
|
||||
theme = Gtk.IconTheme()
|
||||
default = theme.get_default()
|
||||
iconinfo = default.lookup_icon(icon_name, 128, 0)
|
||||
pixbuf, symbol = iconinfo.load_symbolic_for_context(style_context)
|
||||
|
||||
width, height = get_stock_size()
|
||||
pixbuf = pixbuf.scale_simple(width, height,
|
||||
GdkPixbuf.InterpType.BILINEAR)
|
||||
|
||||
return pixbuf
|
||||
|
||||
|
||||
def create_button_image(plugin, image_filename):
|
||||
'''
|
||||
create a pixbuf for the given image_filename sized according to the stock icon size
|
||||
'''
|
||||
path = 'img/'
|
||||
|
||||
return create_pixbuf_from_file_at_size(
|
||||
rb.find_plugin_file(plugin, path + image_filename),
|
||||
*get_stock_size())
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
# -*- Mode: python; coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*-
|
||||
#
|
||||
# Copyright (C) 2014 - fossfreedom
|
||||
# GTK3 port https://github.com/exaile-dev/exaile/blob/master/xlgui/cover.py
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import GdkPixbuf
|
||||
from gi.repository import GObject
|
||||
|
||||
from coverart_browser_prefs import CoverLocale
|
||||
import rb
|
||||
|
||||
|
||||
class CoverWindow(GObject.Object):
|
||||
"""Shows the cover in a simple image viewer"""
|
||||
|
||||
# signals
|
||||
__gsignals__ = {
|
||||
'close-window': (GObject.SIGNAL_RUN_LAST, None, ())
|
||||
}
|
||||
|
||||
def __init__(self, plugin, parent, savedir=None):
|
||||
"""Initializes and shows the cover
|
||||
|
||||
:param plugin: source
|
||||
:type plugin: RBSource
|
||||
:param parent: Parent window to attach to
|
||||
:type parent: Gtk.Window
|
||||
:param savedir: Initial directory for the Save As functionality
|
||||
:type savedir: basestring
|
||||
"""
|
||||
|
||||
super(CoverWindow, self).__init__()
|
||||
cl = CoverLocale()
|
||||
cl.switch_locale(cl.Locale.LOCALE_DOMAIN)
|
||||
self.builder = Gtk.Builder()
|
||||
self.builder.add_from_file(rb.find_plugin_file(plugin,
|
||||
'ui/coverart_window.ui'))
|
||||
self.builder.connect_signals(self)
|
||||
|
||||
self.cover_window = self.builder.get_object('CoverWindow')
|
||||
self.cover_window.connect('destroy', self.send_destroy_signal)
|
||||
self.layout = self.builder.get_object('layout')
|
||||
self.toolbar = self.builder.get_object('toolbar')
|
||||
self.save_as_button = self.builder.get_object('save_as_button')
|
||||
self.zoom_in_button = self.builder.get_object('zoom_in_button')
|
||||
self.zoom_out_button = self.builder.get_object('zoom_out_button')
|
||||
self.zoom_100_button = self.builder.get_object('zoom_100_button')
|
||||
self.zoom_fit_button = self.builder.get_object('zoom_fit_button')
|
||||
self.close_button = self.builder.get_object('close_button')
|
||||
self.image = self.builder.get_object('image')
|
||||
self.statusbar = self.builder.get_object('statusbar')
|
||||
self.scrolledwindow = self.builder.get_object('scrolledwindow')
|
||||
self.scrolledwindow.set_hadjustment(self.layout.get_hadjustment())
|
||||
self.scrolledwindow.set_vadjustment(self.layout.get_vadjustment())
|
||||
|
||||
self.savedir = savedir
|
||||
|
||||
if parent:
|
||||
self.cover_window.set_transient_for(parent)
|
||||
self.cover_window_width = 500
|
||||
self.cover_window_height = 500 + self.toolbar.size_request().height + \
|
||||
self.statusbar.size_request().height
|
||||
self.cover_window.set_default_size(self.cover_window_width, \
|
||||
self.cover_window_height)
|
||||
|
||||
self.min_percent = 1
|
||||
self.max_percent = 500
|
||||
self.ratio = 1.5
|
||||
self.image_interp = GdkPixbuf.InterpType.BILINEAR
|
||||
self.image_fitted = True
|
||||
|
||||
def send_destroy_signal(self, *args):
|
||||
self.emit('close-window')
|
||||
|
||||
def show_all(self, title, pixbuf):
|
||||
self.image_original_pixbuf = pixbuf
|
||||
self.image_pixbuf = self.image_original_pixbuf
|
||||
|
||||
self.cover_window.set_title(title)
|
||||
self.cover_window.show_all()
|
||||
self.set_ratio_to_fit()
|
||||
self.update_widgets()
|
||||
|
||||
def available_image_width(self):
|
||||
"""Returns the available horizontal space for the image"""
|
||||
return self.cover_window.get_size()[0]
|
||||
|
||||
def available_image_height(self):
|
||||
"""Returns the available vertical space for the image"""
|
||||
return self.cover_window.get_size()[1] - \
|
||||
self.toolbar.size_request().height - \
|
||||
self.statusbar.size_request().height
|
||||
|
||||
def center_image(self):
|
||||
"""Centers the image in the layout"""
|
||||
new_x = max(0, int((self.available_image_width() - \
|
||||
self.image_pixbuf.get_width()) / 2))
|
||||
new_y = max(0, int((self.available_image_height() - \
|
||||
self.image_pixbuf.get_height()) / 2))
|
||||
self.layout.move(self.image, new_x, new_y)
|
||||
|
||||
def update_widgets(self):
|
||||
"""Updates image, layout, scrolled window, tool bar and status bar"""
|
||||
# if self.cover_window.window:
|
||||
# self.cover_window.window.freeze_updates()
|
||||
self.apply_zoom()
|
||||
self.layout.set_size(self.image_pixbuf.get_width(), \
|
||||
self.image_pixbuf.get_height())
|
||||
if self.image_fitted or \
|
||||
(self.image_pixbuf.get_width() == self.available_image_width() and \
|
||||
self.image_pixbuf.get_height() == self.available_image_height()):
|
||||
self.scrolledwindow.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.NEVER)
|
||||
else:
|
||||
self.scrolledwindow.set_policy(Gtk.PolicyType.AUTOMATIC,
|
||||
Gtk.PolicyType.AUTOMATIC)
|
||||
percent = int(100 * self.image_ratio)
|
||||
message = str(self.image_original_pixbuf.get_width()) + " x " + \
|
||||
str(self.image_original_pixbuf.get_height()) + \
|
||||
" pixels " + str(percent) + '%'
|
||||
self.zoom_in_button.set_sensitive(percent < self.max_percent)
|
||||
self.zoom_out_button.set_sensitive(percent > self.min_percent)
|
||||
self.statusbar.pop(self.statusbar.get_context_id(''))
|
||||
self.statusbar.push(self.statusbar.get_context_id(''), message)
|
||||
self.image.set_from_pixbuf(self.image_pixbuf)
|
||||
self.center_image()
|
||||
#if self.cover_window.window:
|
||||
# self.cover_window.window.thaw_updates()
|
||||
|
||||
def apply_zoom(self):
|
||||
"""Scales the image if needed"""
|
||||
new_width = int(self.image_original_pixbuf.get_width() * \
|
||||
self.image_ratio)
|
||||
new_height = int(self.image_original_pixbuf.get_height() * \
|
||||
self.image_ratio)
|
||||
if new_width != self.image_pixbuf.get_width() or \
|
||||
new_height != self.image_pixbuf.get_height():
|
||||
self.image_pixbuf = self.image_original_pixbuf.scale_simple(new_width, \
|
||||
new_height, self.image_interp)
|
||||
|
||||
def set_ratio_to_fit(self):
|
||||
"""Calculates and sets the needed ratio to show the full image"""
|
||||
width_ratio = float(self.image_original_pixbuf.get_width()) / \
|
||||
self.available_image_width()
|
||||
height_ratio = float(self.image_original_pixbuf.get_height()) / \
|
||||
self.available_image_height()
|
||||
self.image_ratio = 1 / max(1, width_ratio, height_ratio)
|
||||
|
||||
def on_save_as_button_clicked(self, widget):
|
||||
"""
|
||||
Saves image to user-specified location
|
||||
"""
|
||||
dialog = Gtk.FileChooserDialog(_("Save File"), self.cover_window,
|
||||
Gtk.FileChooserAction.SAVE,
|
||||
(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
|
||||
Gtk.STOCK_SAVE, Gtk.ResponseType.ACCEPT))
|
||||
filename = 'cover.png'
|
||||
dialog.set_current_name(filename)
|
||||
if self.savedir:
|
||||
dialog.set_current_folder(self.savedir)
|
||||
if dialog.run() == Gtk.ResponseType.ACCEPT:
|
||||
filename = dialog.get_filename()
|
||||
lowfilename = filename.lower()
|
||||
if lowfilename.endswith('.jpg') or lowfilename.endswith('.jpeg'):
|
||||
type_ = 'jpeg'
|
||||
else:
|
||||
type_ = 'png'
|
||||
self.image_pixbuf.savev(filename, type_, [None], [None])
|
||||
dialog.destroy()
|
||||
|
||||
def on_zoom_in_button_clicked(self, widget):
|
||||
"""
|
||||
Zooms into the image
|
||||
"""
|
||||
self.image_fitted = False
|
||||
self.image_ratio *= self.ratio
|
||||
self.update_widgets()
|
||||
|
||||
def on_zoom_out_button_clicked(self, widget):
|
||||
"""
|
||||
Zooms out of the image
|
||||
"""
|
||||
self.image_fitted = False
|
||||
self.image_ratio *= 1 / self.ratio
|
||||
self.update_widgets()
|
||||
|
||||
def on_zoom_100_button_clicked(self, widget):
|
||||
"""
|
||||
Restores the original image zoom
|
||||
"""
|
||||
self.image_fitted = False
|
||||
self.image_ratio = 1
|
||||
self.update_widgets()
|
||||
|
||||
def on_zoom_fit_button_clicked(self, widget):
|
||||
"""
|
||||
Zooms the image to fit the window width
|
||||
"""
|
||||
self.image_fitted = True
|
||||
self.set_ratio_to_fit()
|
||||
self.update_widgets()
|
||||
|
||||
def on_close_button_clicked(self, widget):
|
||||
"""
|
||||
Hides the window
|
||||
"""
|
||||
self.cover_window.hide()
|
||||
|
||||
def cover_window_size_allocate(self, widget, allocation):
|
||||
if self.cover_window_width != allocation.width or \
|
||||
self.cover_window_height != allocation.height:
|
||||
if self.image_fitted:
|
||||
self.set_ratio_to_fit()
|
||||
self.update_widgets()
|
||||
self.cover_window_width = allocation.width
|
||||
self.cover_window_height = allocation.height
|
||||
@@ -0,0 +1,81 @@
|
||||
/* ContentFlowAddOn_black, version 2.0
|
||||
* (c) 2008 - 2010 Sebastian Kutsch
|
||||
* <http://www.jacksasylum.eu/ContentFlow/>
|
||||
*
|
||||
* This file is distributed under the terms of the MIT license.
|
||||
* (see http://www.jacksasylum.eu/ContentFlow/LICENSE)
|
||||
*
|
||||
*--------------------------------------------------------------------------*/
|
||||
|
||||
/* ========== ContentFlow ========== */
|
||||
/*
|
||||
* Within this file you can ajust the styling of ContentFlow
|
||||
* to your personal needs. The default styling is the same as found on the
|
||||
* projectpage.
|
||||
*
|
||||
*/
|
||||
|
||||
.ContentFlowAddOn_black {
|
||||
background: black;
|
||||
}
|
||||
|
||||
/* ----- styling of items ----- */
|
||||
.ContentFlowAddOn_black .flow .item .caption {
|
||||
background: url(img/1x1_0.5_black.png);
|
||||
}
|
||||
* html .ContentFlowAddOn_black .flow .item .caption {
|
||||
background-image: none;
|
||||
filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled=true, sizingMethod=scale, src='img/1x1_0.5_black.png');
|
||||
}
|
||||
.ContentFlowAddOn_black .flow .item .caption a,
|
||||
.ContentFlowAddOn_black .flow .item .caption a:link,
|
||||
.ContentFlowAddOn_black .flow .item .caption a:visited,
|
||||
.ContentFlowAddOn_black .flow .item .caption a:active,
|
||||
.ContentFlowAddOn_black .flow .item .caption a:hover {
|
||||
color: black;
|
||||
}
|
||||
/* ----- scrollbar ----- */
|
||||
.ContentFlowAddOn_black .scrollbar {
|
||||
background: url(img/scrollbar_white.png) left center repeat-x;
|
||||
}
|
||||
|
||||
.ContentFlowAddOn_black .scrollbar .slider {
|
||||
background: url(img/slider_white.png) center center no-repeat;
|
||||
}
|
||||
|
||||
/* only for IE <= 6 and a alphatransparent slider image */
|
||||
* html .ContentFlowAddOn_black .scrollbar .slider { background-image: none; }
|
||||
* html .ContentFlowAddOn_black .scrollbar .slider .virtualSlider {
|
||||
filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled=true, sizingMethod=crop, src='img/slider_white.png');
|
||||
}
|
||||
.ContentFlowAddOn_black .scrollbar .slider .position {
|
||||
color:silver;
|
||||
}
|
||||
|
||||
|
||||
/* ----- global caption ----- */
|
||||
.ContentFlowAddOn_black .globalCaption {
|
||||
color: white;
|
||||
}
|
||||
.ContentFlowAddOn_black .globalCaption .caption a,
|
||||
.ContentFlowAddOn_black .globalCaption .caption a:link,
|
||||
.ContentFlowAddOn_black .globalCaption .caption a:visited,
|
||||
.ContentFlowAddOn_black .globalCaption .caption a:active,
|
||||
.ContentFlowAddOn_black .globalCaption .caption a:hover {
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* ----- load indicator ----- */
|
||||
.ContentFlowAddOn_black .loadIndicator {
|
||||
background: url(img/1x1_0.5_black.png);
|
||||
}
|
||||
* html .ContentFlowAddOn_black .loadIndicator {
|
||||
background-image: none;
|
||||
filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled=true, sizingMethod=scale, src='img/1x1_0.5_black.png');
|
||||
}
|
||||
.ContentFlowAddOn_black .loadIndicator .indicator {
|
||||
background: url(img/loader_black.gif) center center no-repeat;
|
||||
}
|
||||
|
||||
/* ================================= */
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
/* ContentFlowAddOn_black, version 2.0
|
||||
* (c) 2008 - 2010 Sebastian Kutsch
|
||||
* <http://www.jacksasylum.eu/ContentFlow/>
|
||||
*
|
||||
* This file is distributed under the terms of the MIT license.
|
||||
* (see http://www.jacksasylum.eu/ContentFlow/LICENSE)
|
||||
*/
|
||||
|
||||
new ContentFlowAddOn ('black', {
|
||||
|
||||
init: function () {
|
||||
this.addStylesheet();
|
||||
},
|
||||
|
||||
ContentFlowConf: {
|
||||
reflectionColor: "#000000" // none, transparent, overlay or hex RGB CSS style #RRGGBB
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
/* ContentFlowAddOn_carousel, version 1.1
|
||||
* (c) 2008 - 2010 Sebastian Kutsch
|
||||
* <http://www.jacksasylum.eu/ContentFlow/>
|
||||
*
|
||||
* This file is distributed under the terms of the MIT license.
|
||||
* (see http://www.jacksasylum.eu/ContentFlow/LICENSE)
|
||||
*
|
||||
*--------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
.ContentFlowAddOn_carousel {
|
||||
|
||||
border: 5px solid #767676;
|
||||
margin: 0 25px;
|
||||
padding: 25px 0px;
|
||||
overflow: visible;
|
||||
/*overflow: hidden;*/
|
||||
}
|
||||
.ContentFlowAddOn_carousel #preButton,
|
||||
.ContentFlowAddOn_carousel #nextButton {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
margin-top: -25px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
.ContentFlowAddOn_carousel #preButton {
|
||||
background: url(img/pre_h.png) center no-repeat;
|
||||
left: -25px;
|
||||
left: -28px;
|
||||
}
|
||||
* html .ContentFlowAddOn_carousel #preButton {
|
||||
background-image: none;
|
||||
filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled=true, sizingMethod=scale, src='img/pre_h.png');
|
||||
}
|
||||
.ContentFlowAddOn_carousel #preButton:hover {
|
||||
background: url(img/pre_h.png) center no-repeat;
|
||||
}
|
||||
.ContentFlowAddOn_carousel #nextButton {
|
||||
background: url(img/next_h.png) center no-repeat;
|
||||
right: -25px;
|
||||
right: -28px;
|
||||
}
|
||||
* html .ContentFlowAddOn_carousel #nextButton {
|
||||
background-image: none;
|
||||
filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled=true, sizingMethod=scale, src='img/next_h.png');
|
||||
}
|
||||
.ContentFlowAddOn_carousel #nextButton:hover {
|
||||
background: url(img/next_h.png) center no-repeat;
|
||||
}
|
||||
|
||||
/* ----- styling of items ----- */
|
||||
.ContentFlowAddOn_carousel .showCaption .item .caption {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
|
||||
/* ----- global caption ----- */
|
||||
.ContentFlowAddOn_carousel .globalCaption {
|
||||
margin-top: -3em;
|
||||
}
|
||||
|
||||
|
||||
/* ================================= */
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
/* ContentFlowAddOn_carousel, version 1.1
|
||||
* (c) 2008 - 2010 Sebastian Kutsch
|
||||
* <http://www.jacksasylum.eu/ContentFlow/>
|
||||
*
|
||||
* This file is distributed under the terms of the MIT license.
|
||||
* (see http://www.jacksasylum.eu/ContentFlow/LICENSE)
|
||||
*/
|
||||
|
||||
new ContentFlowAddOn ('carousel', {
|
||||
|
||||
conf: {
|
||||
shownItems: 3,
|
||||
showCaption: true,
|
||||
width: 75,
|
||||
height: 75,
|
||||
space:0.0
|
||||
},
|
||||
|
||||
|
||||
init: function() {
|
||||
this.addStylesheet();
|
||||
},
|
||||
|
||||
onloadInit: function (flow) {
|
||||
},
|
||||
|
||||
afterContentFlowInit: function (flow) {
|
||||
var SI = flow.getAddOnConf('carousel').shownItems;
|
||||
var c = flow.Container;
|
||||
var ac = flow.getAddOnConf('carousel')
|
||||
if (ac.showCaption) {
|
||||
$CF(flow.Flow).addClassName('showCaption');
|
||||
}
|
||||
|
||||
var p = document.createElement('div');
|
||||
p.id = "preButton";
|
||||
var pre = function () {
|
||||
var item = flow._activeItem;
|
||||
for (var i=0; i< SI; i++) { item = item.pre; }
|
||||
flow.moveToItem(item);
|
||||
}
|
||||
p.onclick = pre;
|
||||
c.appendChild(p);
|
||||
|
||||
var n = document.createElement('div');
|
||||
n.id = "nextButton";
|
||||
var next = function () {
|
||||
var item = flow._activeItem;
|
||||
for (var i=0; i< SI; i++) { item = item.next; }
|
||||
flow.moveToItem(item);
|
||||
}
|
||||
n.onclick = next;
|
||||
c.appendChild(n);
|
||||
|
||||
flow.Flow.style.fontSize = 12*(flow.maxHeight / 150) +"px";
|
||||
if (flow.Browser.IE) {
|
||||
window.setTimeout(function () {flow.Flow.style.overflow = "hidden"}, 1000);
|
||||
}
|
||||
else {
|
||||
flow.Flow.style.overflow = "hidden";
|
||||
}
|
||||
|
||||
flow.setConfig({visibleItems: Math.ceil((flow.getAddOnConf('carousel').shownItems - 1)/2) + 1});
|
||||
},
|
||||
|
||||
ContentFlowConf: {
|
||||
scaleFactorLandscape: "max", // scale factor of landscape images ('max' := height= maxItemHeight)
|
||||
scaleFactorPortrait: "max",
|
||||
fixItemSize: true,
|
||||
relativeItemPosition: "center", // align top/above, bottom/below, left, right, center of position coordinate
|
||||
visibleItems: 2, // how man item are visible on each side (-1 := auto)
|
||||
reflectionHeight: 0, // float (relative to original image height)
|
||||
|
||||
|
||||
/* ==================== actions ==================== */
|
||||
onclickInactiveItem : function (item) {
|
||||
this.conf.onclickActiveItem(item);
|
||||
return false;
|
||||
},
|
||||
|
||||
/* ==================== calculations ==================== */
|
||||
|
||||
calcStepWidth: function(diff) {
|
||||
var vI = this.conf.visibleItems;
|
||||
var items = this.items.length;
|
||||
items = items == 0 ? 1 : items;
|
||||
var absDiff = Math.abs(diff);
|
||||
if (absDiff > vI) {
|
||||
if (diff > 0) {
|
||||
var stepwidth = diff - vI;
|
||||
} else {
|
||||
var stepwidth = diff + vI;
|
||||
}
|
||||
} else if (vI >= items) {
|
||||
var stepwidth = diff / items;
|
||||
} else {
|
||||
var c = this.getAddOnConf('carousel');
|
||||
var f = 0.1 * 2/3 * c.shownItems * diff/absDiff;
|
||||
var d = diff * ( vI / items);
|
||||
var stepwidth = absDiff > 0.1 ? f : d*8;
|
||||
}
|
||||
return stepwidth;
|
||||
},
|
||||
|
||||
|
||||
calcSize: function (item) {
|
||||
var c = this.getAddOnConf('carousel');
|
||||
var a = c.width / c.height;
|
||||
//if (this.conf.verticalFlow) a = 1/a;
|
||||
var h = 3/c.shownItems / a;
|
||||
//if (this.conf.verticalFlow) h *= 2/3;
|
||||
var w = h * a;
|
||||
return {width: w, height: h};
|
||||
},
|
||||
|
||||
calcCoordinates: function (item) {
|
||||
var rP = item.relativePosition;
|
||||
var c = this.getAddOnConf('carousel');
|
||||
var w = item.size.width;
|
||||
//if (this.conf.verticalFlow) w = item.size.height;
|
||||
var x = rP*w/2*(1 + c.space) *this.conf.scaleFactor - w* (c.shownItems % 2 ? 0 : 0.5) / 1.4;
|
||||
if (this.conf.verticalFlow) x *= 2*2/3;
|
||||
var y = 0;
|
||||
|
||||
return {x: x, y: y};
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
/* ContentFlowAddOn_roundabout, version 3.0
|
||||
* (c) 2008 - 2010 Sebastian Kutsch
|
||||
* <http://www.jacksasylum.eu/ContentFlow/>
|
||||
*
|
||||
* This file is distributed under the terms of the MIT license.
|
||||
* (see http://www.jacksasylum.eu/ContentFlow/LICENSE)
|
||||
*/
|
||||
|
||||
new ContentFlowAddOn ('roundabout', {
|
||||
|
||||
ContentFlowConf: {
|
||||
circularFlow: true,
|
||||
visibleItems: -1,
|
||||
relativeItemPosition: "top center",
|
||||
endOpacity: 0.5,
|
||||
|
||||
/*
|
||||
* calculates the size of the item at its relative position x
|
||||
* returns a size object
|
||||
*/
|
||||
calcSize: function (item) {
|
||||
var rP = item.relativePosition;
|
||||
//var rPN = relativePositionNormed;
|
||||
//var vI = rPN != 0 ? rP/rPN : 0 ; // visible Items
|
||||
|
||||
var h = 1/(Math.abs(rP)+1);
|
||||
var w = h;
|
||||
return {width: w, height: h};
|
||||
},
|
||||
|
||||
/*
|
||||
* calculates the position of an item within the flow element
|
||||
* returns a vector object
|
||||
*/
|
||||
calcCoordinates: function (item) {
|
||||
var rP = item.relativePosition;
|
||||
var rPN = item.relativePositionNormed;
|
||||
var vI = rPN != 0 ? rP/rPN : 0 ; // visible Items
|
||||
|
||||
var f = 1 - 1/Math.exp( Math.abs(rP)*0.75);
|
||||
var x = item.side * vI/(vI+1)* f;
|
||||
var y = 1;
|
||||
|
||||
var f = Math.sin(Math.PI * (rP*(1+1/(rP*rP+1))) / (vI+1));
|
||||
var x = vI/(vI+1)* f;
|
||||
var y = 1 - Math.abs(rP)*1.5/(vI+1);
|
||||
|
||||
return {x: x, y: y};
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
/* ContentFlowAddOn_vertical, version 2.0
|
||||
* (c) 2008 - 2010 Sebastian Kutsch
|
||||
* <http://www.jacksasylum.eu/ContentFlow/>
|
||||
*
|
||||
* This file is distributed under the terms of the MIT license.
|
||||
* (see http://www.jacksasylum.eu/ContentFlow/LICENSE)
|
||||
*/
|
||||
|
||||
|
||||
new ContentFlowAddOn ('vertical', {
|
||||
|
||||
ContentFlowConf: {
|
||||
relativeItemPosition: "center", // top, bottom, left, right, center
|
||||
verticalFlow: true, // turn ContentFlow 90 degree counterclockwise
|
||||
reflectionHeight: 0,
|
||||
|
||||
calcCoordinates: function (item) {
|
||||
var rP = item.relativePosition;
|
||||
var rPN = item.relativePositionNormed;
|
||||
var vI = rPN != 0 ? rP/rPN : 0 ; // visible Items
|
||||
|
||||
var f = 1 - 1/Math.exp( Math.abs(rP)*0.75);
|
||||
var x = item.side * vI/(vI+1)* f;
|
||||
var y = 0;
|
||||
|
||||
return {x: x, y: y};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
/* ContentFlowAddOn_white, version 2.0
|
||||
* (c) 2008 - 2010 Sebastian Kutsch
|
||||
* <http://www.jacksasylum.eu/ContentFlow/>
|
||||
*
|
||||
* This file is distributed under the terms of the MIT license.
|
||||
* (see http://www.jacksasylum.eu/ContentFlow/LICENSE)
|
||||
*
|
||||
*--------------------------------------------------------------------------*/
|
||||
|
||||
/* ========== ContentFlow ========== */
|
||||
/*
|
||||
* Within this file you can ajust the styling of ContentFlow
|
||||
* to your personal needs. The default styling is the same as found on the
|
||||
* projectpage.
|
||||
*
|
||||
*/
|
||||
|
||||
.ContentFlowAddOn_white {
|
||||
background: white;
|
||||
}
|
||||
|
||||
/* ----- styling of items ----- */
|
||||
.ContentFlowAddOn_white .flow .item .caption {
|
||||
background: url(img/1x1_0.5_white.png);
|
||||
}
|
||||
* html .ContentFlowAddOn_white .flow .item .caption {
|
||||
background-image: none;
|
||||
filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled=true, sizingMethod=scale, src='img/1x1_0.5_white.png');
|
||||
}
|
||||
.ContentFlowAddOn_white .flow .item .caption a,
|
||||
.ContentFlowAddOn_white .flow .item .caption a:link,
|
||||
.ContentFlowAddOn_white .flow .item .caption a:visited,
|
||||
.ContentFlowAddOn_white .flow .item .caption a:active,
|
||||
.ContentFlowAddOn_white .flow .item .caption a:hover {
|
||||
color: black;
|
||||
}
|
||||
/* ----- scrollbar ----- */
|
||||
.ContentFlowAddOn_white .scrollbar {
|
||||
background: url(img/scrollbar_black.png) left center repeat-x;
|
||||
}
|
||||
|
||||
.ContentFlowAddOn_white .scrollbar .slider {
|
||||
background: url(img/slider_black.png) center center no-repeat;
|
||||
}
|
||||
|
||||
/* only for IE <= 6 and a alphatransparent slider image */
|
||||
* html .ContentFlowAddOn_white .scrollbar .slider { background-image: none; }
|
||||
* html .ContentFlowAddOn_white .scrollbar .slider .virtualSlider {
|
||||
filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled=true, sizingMethod=crop, src='img/slider_black.png');
|
||||
}
|
||||
.ContentFlowAddOn_white .scrollbar .slider .position {
|
||||
color:gray;
|
||||
}
|
||||
|
||||
|
||||
/* ----- global caption ----- */
|
||||
.ContentFlowAddOn_white .globalCaption {
|
||||
color: black;
|
||||
}
|
||||
.ContentFlowAddOn_white .globalCaption .caption a,
|
||||
.ContentFlowAddOn_white .globalCaption .caption a:link,
|
||||
.ContentFlowAddOn_white .globalCaption .caption a:visited,
|
||||
.ContentFlowAddOn_white .globalCaption .caption a:active,
|
||||
.ContentFlowAddOn_white .globalCaption .caption a:hover {
|
||||
color: black;
|
||||
}
|
||||
|
||||
/* ----- load indicator ----- */
|
||||
.ContentFlowAddOn_white .loadIndicator {
|
||||
background: url(img/1x1_0.5_white.png);
|
||||
}
|
||||
* html .ContentFlowAddOn_white .loadIndicator {
|
||||
background-image: none;
|
||||
filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled=true, sizingMethod=scale, src='img/1x1_0.5_white.png');
|
||||
}
|
||||
.ContentFlowAddOn_white .loadIndicator .indicator {
|
||||
background: url(img/loader_white.gif) center center no-repeat;
|
||||
}
|
||||
|
||||
/* ================================= */
|
||||
@@ -0,0 +1,19 @@
|
||||
/* ContentFlowAddOn_white, version 2.0
|
||||
* (c) 2008 - 2010 Sebastian Kutsch
|
||||
* <http://www.jacksasylum.eu/ContentFlow/>
|
||||
*
|
||||
* This file is distributed under the terms of the MIT license.
|
||||
* (see http://www.jacksasylum.eu/ContentFlow/LICENSE)
|
||||
*/
|
||||
|
||||
new ContentFlowAddOn ('white', {
|
||||
|
||||
init: function () {
|
||||
this.addStylesheet();
|
||||
},
|
||||
|
||||
ContentFlowConf: {
|
||||
reflectionColor: "#ffffff" // none, transparent, overlay or hex RGB CSS style #RRGGBB
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2007 - 2010 Sebastian Kutsch
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -0,0 +1,250 @@
|
||||
|
||||
/* ========== ContentFlow ========== */
|
||||
/*
|
||||
* default style to look nice
|
||||
*/
|
||||
|
||||
.ContentFlow {
|
||||
}
|
||||
.ContentFlow .flow {
|
||||
/*border: 1px solid green;*/
|
||||
}
|
||||
.ContentFlow .flow * {
|
||||
}
|
||||
|
||||
.ContentFlow .flow .item {
|
||||
/*border: 1px solid red;*/
|
||||
}
|
||||
.ContentFlow .flow .item canvas.content {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
/*border: 1px solid yellow;*/
|
||||
}
|
||||
.ContentFlow .flow .item img.content {
|
||||
/*border: 1px solid yellow;*/
|
||||
width: 100%;
|
||||
}
|
||||
.ContentFlow .flow .item img.reflection,
|
||||
.ContentFlow .flow .item canvas.reflection {
|
||||
width: 100%;
|
||||
}
|
||||
/* ----- styling of items ----- */
|
||||
.ContentFlow .flow .item.active {
|
||||
cursor: pointer;
|
||||
}
|
||||
.ContentFlow .flow .item .caption {
|
||||
font-size: 100%;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
color: white;
|
||||
max-height: 30%;
|
||||
bottom: 10%;
|
||||
background: url(img/1x1_0.5_black.png);
|
||||
width: 100%;
|
||||
}
|
||||
* html .ContentFlow .flow .item .caption {
|
||||
background-image: none;
|
||||
filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled=true, sizingMethod=scale, src='img/1x1_0.5_black.png');
|
||||
}
|
||||
.ContentFlow .flow .item .caption a,
|
||||
.ContentFlow .flow .item .caption a:link,
|
||||
.ContentFlow .flow .item .caption a:visited,
|
||||
.ContentFlow .flow .item .caption a:active,
|
||||
.ContentFlow .flow .item .caption a:hover {
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
font-style: italic;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
.ContentFlow .flow .item .caption a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.ContentFlow .flow .item.active .caption {
|
||||
/*display: block;*/ /* uncomment to show caption inside item */
|
||||
}
|
||||
|
||||
/* ----- scrollbar ----- */
|
||||
.ContentFlow .scrollbar {
|
||||
width: 50%;
|
||||
margin: 0px auto;
|
||||
margin-top: 10px;
|
||||
height: 16px;
|
||||
background: url(img/scrollbar_white.png) left center repeat-x;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.ContentFlow .scrollbar .slider {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: url(img/slider_white.png) center center no-repeat;
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
/* only for IE <= 6 and a alphatransparent slider image */
|
||||
* html .ContentFlow .scrollbar .slider { background-image: none; }
|
||||
* html .ContentFlow .scrollbar .slider .virtualSlider {
|
||||
filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled=true, sizingMethod=crop, src='img/slider_white.png');
|
||||
}
|
||||
.ContentFlow .scrollbar .slider .position {
|
||||
top: 120%;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: silver
|
||||
}
|
||||
|
||||
/* ----- global caption ----- */
|
||||
.ContentFlow .globalCaption {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
height: 20px;
|
||||
margin: 2em auto;
|
||||
}
|
||||
.ContentFlow .globalCaption .caption {
|
||||
}
|
||||
.ContentFlow .globalCaption .caption a,
|
||||
.ContentFlow .globalCaption .caption a:link,
|
||||
.ContentFlow .globalCaption .caption a:visited,
|
||||
.ContentFlow .globalCaption .caption a:active,
|
||||
.ContentFlow .globalCaption .caption a:hover {
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
font-style: italic;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.ContentFlow .globalCaption .caption a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* ----- load indicator ----- */
|
||||
.ContentFlow .loadIndicator {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
background: black;
|
||||
}
|
||||
.ContentFlow .loadIndicator .indicator {
|
||||
background: url(img/loader.gif) center center no-repeat;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
* html .ContentFlow .loadIndicator .indicator {
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
/* ================================= */
|
||||
|
||||
|
||||
|
||||
|
||||
/* ========== ContentFlow ========== */
|
||||
/*
|
||||
* This is the basic CSS file needed for the correct functioning of ContentFlow.
|
||||
* DON'T CHANGE IT.
|
||||
*
|
||||
*/
|
||||
.ContentFlow {
|
||||
position: relative; /* needed so overlay dimensions are constrained to the ContentFlow */
|
||||
overflow: hidden;
|
||||
}
|
||||
.ContentFlow * {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
/*border: none;*/
|
||||
}
|
||||
.ContentFlow img {
|
||||
-ms-interpolation-mode: bicubic;
|
||||
}
|
||||
|
||||
.ContentFlow .mouseoverCheckElement {
|
||||
position: absolute;
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
left: 0px;
|
||||
/*display: none;*/
|
||||
visibility: hidden;
|
||||
}
|
||||
.ContentFlow:hover .mouseoverCheckElement {
|
||||
left: 1px;
|
||||
/*width: 1px;*/
|
||||
/*left: -1px;*/
|
||||
/*background-color: red;*/
|
||||
}
|
||||
.ContentFlow .flow {
|
||||
position: relative; /* needed so that items can be positioned relative to flow*/
|
||||
z-index: 0; /* need so every item has a z-index relative to the flow-box */
|
||||
visibility: hidden; /* needed so that content is hidden while loading */
|
||||
width: 100%; /* needed for IE6 */
|
||||
margin: 0 auto;
|
||||
}
|
||||
.ContentFlow .flow.hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.ContentFlow .flow .item {
|
||||
position: absolute; /* needed */
|
||||
visibility: hidden;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
}
|
||||
.ContentFlow .flow .item.active {
|
||||
}
|
||||
.ContentFlow .flow .item .content {
|
||||
display: block;
|
||||
}
|
||||
.ContentFlow .flow .item div.content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.ContentFlow .flow .item .label {
|
||||
display: none;
|
||||
}
|
||||
.ContentFlow .flow .item .reflection {
|
||||
display: block;
|
||||
}
|
||||
.ContentFlow .flow .item canvas.reflection {
|
||||
margin-top: -1px; /* for FF */
|
||||
}
|
||||
.ContentFlow .flow .item .caption {
|
||||
position: absolute; /* needed */
|
||||
display: none; /* needed to hide it on inactive items */
|
||||
}
|
||||
.ContentFlow .flow .item.active .caption {
|
||||
/*display: block;*/ /* uncomment to show caption inside item */
|
||||
}
|
||||
|
||||
/* ----- scrollbar ----- */
|
||||
|
||||
.ContentFlow .scrollbar {
|
||||
position: relative; /* needed for z-index */
|
||||
z-index: 1; /* set above flow */
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.ContentFlow .scrollbar .slider {
|
||||
position: absolute; /* needed */
|
||||
}
|
||||
* html .ContentFlow .scrollbar .slider .virtualSlider {
|
||||
height: 100%;
|
||||
}
|
||||
.ContentFlow .scrollbar .slider .position {
|
||||
position: absolute; /* needed */
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ----- global caption ----- */
|
||||
.ContentFlow .globalCaption {
|
||||
position: relative; /* needed for z-index */
|
||||
z-index: 1; /* set above flow */
|
||||
}
|
||||
/* ----- load indicator ----- */
|
||||
.ContentFlow .loadIndicator {
|
||||
position: absolute; /* needed */
|
||||
z-index: 65000; /* set above everything */
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
|
||||
<head>
|
||||
<link rel="stylesheet" title="Standard" href="styles.css" type="text/css" media="screen" />
|
||||
|
||||
<!--<style>
|
||||
|
||||
</style> -->
|
||||
|
||||
<style type="text/css">
|
||||
.className{
|
||||
width:270px;
|
||||
height:150px;
|
||||
position:absolute;
|
||||
left:50%;
|
||||
top:50%;
|
||||
margin:-75px 0 0 -135px;
|
||||
}
|
||||
|
||||
.className p{
|
||||
font-size:22px;
|
||||
margin:45px 10px 10px;
|
||||
color: #BACKGROUND_COLOUR;
|
||||
text-align:center;
|
||||
position:absolute;
|
||||
}
|
||||
|
||||
body{
|
||||
background: #BACKGROUND_COLOUR;
|
||||
font-size:0.825em;
|
||||
font-family:Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="className">
|
||||
<p>Use the Search and Filter options to display covers
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
Depois Largura: | Altura: | Tamanho: 82 B |
|
Depois Largura: | Altura: | Tamanho: 82 B |
|
Depois Largura: | Altura: | Tamanho: 43 B |
@@ -0,0 +1,241 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="744.09448819"
|
||||
height="1052.3622047"
|
||||
id="svg2"
|
||||
sodipodi:version="0.32"
|
||||
inkscape:version="0.47 r22583"
|
||||
version="1.1"
|
||||
sodipodi:docname="New document 1.2009_11_04_18_16_23.0.2009_11_29_20_35_24.0.svg">
|
||||
<defs
|
||||
id="defs4">
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient3612">
|
||||
<stop
|
||||
style="stop-color:#ffffff;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3614" />
|
||||
<stop
|
||||
style="stop-color:#ffffff;stop-opacity:0;"
|
||||
offset="1"
|
||||
id="stop3616" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient3157">
|
||||
<stop
|
||||
style="stop-color:#ffffff;stop-opacity:0.57391304;"
|
||||
offset="0"
|
||||
id="stop3159" />
|
||||
<stop
|
||||
style="stop-color:#ffffff;stop-opacity:0;"
|
||||
offset="1"
|
||||
id="stop3161" />
|
||||
</linearGradient>
|
||||
<inkscape:perspective
|
||||
sodipodi:type="inkscape:persp3d"
|
||||
inkscape:vp_x="0 : 526.18109 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_z="744.09448 : 526.18109 : 1"
|
||||
inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
|
||||
id="perspective10" />
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3157"
|
||||
id="radialGradient3163"
|
||||
cx="307.14285"
|
||||
cy="582.36218"
|
||||
fx="307.14285"
|
||||
fy="582.36218"
|
||||
r="158.57143"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3157"
|
||||
id="radialGradient3187"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
cx="307.14285"
|
||||
cy="582.36218"
|
||||
fx="307.14285"
|
||||
fy="582.36218"
|
||||
r="158.57143" />
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath3195">
|
||||
<path
|
||||
sodipodi:type="arc"
|
||||
style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
id="path3197"
|
||||
sodipodi:cx="268.57144"
|
||||
sodipodi:cy="569.50507"
|
||||
sodipodi:rx="171.42857"
|
||||
sodipodi:ry="171.42857"
|
||||
d="M 440.00002,569.50507 A 171.42857,171.42857 0 1 1 97.142868,569.50507 A 171.42857,171.42857 0 1 1 440.00002,569.50507 z"
|
||||
transform="matrix(0.5151267,0,0,0.5151267,47.101979,154.22678)" />
|
||||
</clipPath>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath3205">
|
||||
<path
|
||||
sodipodi:type="arc"
|
||||
style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
id="path3207"
|
||||
sodipodi:cx="268.57144"
|
||||
sodipodi:cy="569.50507"
|
||||
sodipodi:rx="171.42857"
|
||||
sodipodi:ry="171.42857"
|
||||
d="M 440.00002,569.50507 A 171.42857,171.42857 0 1 1 97.142868,569.50507 A 171.42857,171.42857 0 1 1 440.00002,569.50507 z"
|
||||
transform="matrix(0.5151267,0,0,0.5151267,385.50521,158.17142)" />
|
||||
</clipPath>
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3612"
|
||||
id="radialGradient3618"
|
||||
cx="307.14285"
|
||||
cy="582.36218"
|
||||
fx="307.14285"
|
||||
fy="582.36218"
|
||||
r="158.57143"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<radialGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3612"
|
||||
id="radialGradient3639"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
cx="307.14285"
|
||||
cy="582.36218"
|
||||
fx="307.14285"
|
||||
fy="582.36218"
|
||||
r="158.57143" />
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
gridtolerance="10000"
|
||||
guidetolerance="10"
|
||||
objecttolerance="10"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.35"
|
||||
inkscape:cx="-159.28571"
|
||||
inkscape:cy="405.71429"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="g3620"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1400"
|
||||
inkscape:window-height="1002"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="0" />
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<g
|
||||
id="g3620">
|
||||
<g
|
||||
id="g2835"
|
||||
transform="matrix(-1,0,0,1,370.9006,0)">
|
||||
<path
|
||||
d="m 440.00002,569.50507 c 0,94.67738 -76.75119,171.42857 -171.42858,171.42857 -94.67739,0 -171.428572,-76.75119 -171.428572,-171.42857 0,-94.67739 76.751182,-171.42858 171.428572,-171.42858 94.67739,0 171.42858,76.75119 171.42858,171.42858 z"
|
||||
sodipodi:ry="171.42857"
|
||||
sodipodi:rx="171.42857"
|
||||
sodipodi:cy="569.50507"
|
||||
sodipodi:cx="268.57144"
|
||||
id="path3173"
|
||||
style="fill:#4d4d4d;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
sodipodi:type="arc"
|
||||
transform="matrix(0.5151267,0,0,0.5151267,47.101979,154.22678)" />
|
||||
<path
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
d="m 193.02128,416.31621 0,17.11698 -69.31988,0 0,28.32175 69.31988,0 0,17.11698 27.09543,-15.63248 27.08251,-15.64537 -27.08251,-15.63248 -27.09543,-15.64538 z"
|
||||
id="rect3165" />
|
||||
<path
|
||||
d="m 440.00002,569.50507 c 0,94.67738 -76.75119,171.42857 -171.42858,171.42857 -94.67739,0 -171.428572,-76.75119 -171.428572,-171.42857 0,-94.67739 76.751182,-171.42858 171.428572,-171.42858 94.67739,0 171.42858,76.75119 171.42858,171.42858 z"
|
||||
sodipodi:ry="171.42857"
|
||||
sodipodi:rx="171.42857"
|
||||
sodipodi:cy="569.50507"
|
||||
sodipodi:cx="268.57144"
|
||||
id="path3175"
|
||||
style="fill:none;stroke:#808080;stroke-width:10.80000019;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
|
||||
sodipodi:type="arc"
|
||||
transform="matrix(0.4679068,0,0,0.4679068,59.783914,181.11879)" />
|
||||
</g>
|
||||
<path
|
||||
transform="matrix(0.5847384,0,0,0.5847384,30.872504,70.269726)"
|
||||
d="m 465.71428,582.36218 a 158.57143,158.57143 0 1 1 -317.14285,0 158.57143,158.57143 0 1 1 317.14285,0 z"
|
||||
sodipodi:ry="158.57143"
|
||||
sodipodi:rx="158.57143"
|
||||
sodipodi:cy="582.36218"
|
||||
sodipodi:cx="307.14285"
|
||||
id="path2385"
|
||||
style="fill:url(#radialGradient3618);fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
sodipodi:type="arc" />
|
||||
</g>
|
||||
<g
|
||||
id="g3627"
|
||||
transform="translate(314.28571,5.7142857)">
|
||||
<g
|
||||
id="g3629">
|
||||
<path
|
||||
transform="matrix(0.5151267,0,0,0.5151267,47.101979,154.22678)"
|
||||
sodipodi:type="arc"
|
||||
style="fill:#4d4d4d;fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
id="path3631"
|
||||
sodipodi:cx="268.57144"
|
||||
sodipodi:cy="569.50507"
|
||||
sodipodi:rx="171.42857"
|
||||
sodipodi:ry="171.42857"
|
||||
d="m 440.00002,569.50507 c 0,94.67738 -76.75119,171.42857 -171.42858,171.42857 -94.67739,0 -171.428572,-76.75119 -171.428572,-171.42857 0,-94.67739 76.751182,-171.42858 171.428572,-171.42858 94.67739,0 171.42858,76.75119 171.42858,171.42858 z" />
|
||||
<path
|
||||
id="path3633"
|
||||
d="m 193.02128,416.31621 0,17.11698 -69.31988,0 0,28.32175 69.31988,0 0,17.11698 27.09543,-15.63248 27.08251,-15.64537 -27.08251,-15.63248 -27.09543,-15.64538 z"
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none" />
|
||||
<path
|
||||
transform="matrix(0.4679068,0,0,0.4679068,59.783914,181.11879)"
|
||||
sodipodi:type="arc"
|
||||
style="fill:none;stroke:#808080;stroke-width:10.80000019;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
|
||||
id="path3635"
|
||||
sodipodi:cx="268.57144"
|
||||
sodipodi:cy="569.50507"
|
||||
sodipodi:rx="171.42857"
|
||||
sodipodi:ry="171.42857"
|
||||
d="m 440.00002,569.50507 c 0,94.67738 -76.75119,171.42857 -171.42858,171.42857 -94.67739,0 -171.428572,-76.75119 -171.428572,-171.42857 0,-94.67739 76.751182,-171.42858 171.428572,-171.42858 94.67739,0 171.42858,76.75119 171.42858,171.42858 z" />
|
||||
</g>
|
||||
<path
|
||||
sodipodi:type="arc"
|
||||
style="fill:url(#radialGradient3639);fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
id="path3637"
|
||||
sodipodi:cx="307.14285"
|
||||
sodipodi:cy="582.36218"
|
||||
sodipodi:rx="158.57143"
|
||||
sodipodi:ry="158.57143"
|
||||
d="m 465.71428,582.36218 c 0,87.57658 -70.99485,158.57143 -158.57143,158.57143 -87.57658,0 -158.57142,-70.99485 -158.57142,-158.57143 0,-87.57658 70.99484,-158.57142 158.57142,-158.57142 87.57658,0 158.57143,70.99484 158.57143,158.57142 z"
|
||||
transform="matrix(0.5847384,0,0,0.5847384,30.872504,70.269726)" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Depois Largura: | Altura: | Tamanho: 9.2 KiB |
|
Depois Largura: | Altura: | Tamanho: 8.0 KiB |
|
Depois Largura: | Altura: | Tamanho: 8.0 KiB |
|
Depois Largura: | Altura: | Tamanho: 8.0 KiB |
|
Depois Largura: | Altura: | Tamanho: 2.0 KiB |
|
Depois Largura: | Altura: | Tamanho: 3.0 KiB |
|
Depois Largura: | Altura: | Tamanho: 1.9 KiB |
|
Depois Largura: | Altura: | Tamanho: 3.0 KiB |
|
Depois Largura: | Altura: | Tamanho: 82 B |
|
Depois Largura: | Altura: | Tamanho: 82 B |
|
Depois Largura: | Altura: | Tamanho: 175 B |
|
Depois Largura: | Altura: | Tamanho: 178 B |
@@ -0,0 +1,55 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
|
||||
<head>
|
||||
<link rel="stylesheet" title="Standard" href="styles.css" type="text/css" media="screen" />
|
||||
|
||||
<!-- styles.css contains white for background colour - we'll need an equivalent css statement for a black background -->
|
||||
<script language="JavaScript" type="text/javascript" src="contentflow.js" load="#BACKGROUND_COLOUR #ADDON"></script>
|
||||
<script style="text/javascript">
|
||||
var cf = new ContentFlow('contentFlow',
|
||||
{reflectionColor: "#000000",
|
||||
endOpacity : 0.3,
|
||||
circularFlow: true,
|
||||
startItem: #START,
|
||||
scaleFactor: #FACTOR,
|
||||
visibleItems: 7
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
body{
|
||||
background: #BACKGROUND_COLOUR;
|
||||
color: #FOREGROUND_COLOUR;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
#MAXCOVERS
|
||||
<div class="maincontent">
|
||||
<div style="width: #WIDTHpx; margin: 0px auto;">
|
||||
|
||||
<!-- ===== FLOW ===== -->
|
||||
<div id="contentFlow" class="ContentFlow">
|
||||
<!-- should be place before flow so that contained images will be loaded first -->
|
||||
<div class="loadIndicator"><div class="indicator"></div></div>
|
||||
|
||||
<div class="flow">
|
||||
|
||||
#ITEMS
|
||||
|
||||
</div>
|
||||
#GLOBAL_CAPTION
|
||||
<!--
|
||||
<div class="scrollbar">
|
||||
<div class="slider"></div>
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,231 @@
|
||||
body {
|
||||
background: white;
|
||||
color: white;
|
||||
font-size: 8pt;
|
||||
font-family: sans-serif;
|
||||
margin: 0;
|
||||
padding: 0 7%;
|
||||
}
|
||||
|
||||
.className{
|
||||
background-color:#338BC7;
|
||||
width:270px;
|
||||
height:150px;
|
||||
position:relative;
|
||||
|
||||
-moz-border-radius:6px;
|
||||
-webkit-border-radius:6px;
|
||||
border-radius:6px;
|
||||
}
|
||||
|
||||
a,
|
||||
a:link,
|
||||
a:visited,
|
||||
a:active,
|
||||
a:hover {
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.clear { clear: both; }
|
||||
|
||||
img { border: none; }
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
text-decoration: underline;
|
||||
font-family: serif;
|
||||
color: white;
|
||||
font-size: 1.8em;
|
||||
}
|
||||
h2 {
|
||||
margin-top: 2.5em;
|
||||
font-size: 1.7em;
|
||||
text-decoration: underline;
|
||||
}
|
||||
h3 {
|
||||
margin-top: 2em;
|
||||
font-size: 1.3em;
|
||||
text-decoration: underline;
|
||||
}
|
||||
p {
|
||||
line-height: 150%;
|
||||
text-align: justify;
|
||||
}
|
||||
pre {
|
||||
font-family: sans-serif;
|
||||
background: #222;
|
||||
}
|
||||
.simpleBlack {
|
||||
background: black;
|
||||
width: 80ex;
|
||||
margin: 0 auto;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: monospace;
|
||||
font-size: 0.9em;
|
||||
line-height: 150%;
|
||||
}
|
||||
|
||||
dl {
|
||||
margin-left: 4ex;
|
||||
}
|
||||
|
||||
dl dt {
|
||||
font-size: 0.9em;
|
||||
font-weight: bold;
|
||||
color: #eee;
|
||||
font-family: monospace;
|
||||
}
|
||||
dl dd {
|
||||
margin-bottom: 1.5em;
|
||||
color: #ccc;
|
||||
font-size: 0.85em;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
|
||||
ol {
|
||||
}
|
||||
ol li {
|
||||
margin-bottom: 1em;
|
||||
line-height: 150%;
|
||||
}
|
||||
|
||||
ul {
|
||||
}
|
||||
ul li {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
#title {
|
||||
margin: 0px auto;
|
||||
}
|
||||
|
||||
#sponsor {
|
||||
position: absolute;
|
||||
right: 7%;
|
||||
font-size: 10px;
|
||||
height: 66px;
|
||||
line-height: 66px;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
div.maincontent {
|
||||
margin: 0px auto 20pt auto;
|
||||
}
|
||||
|
||||
#menu {
|
||||
display: block;
|
||||
text-align: center;
|
||||
background: #222;
|
||||
padding: 1em;
|
||||
margin: 2em auto 4em auto;
|
||||
}
|
||||
|
||||
#menu li {
|
||||
display: inline;
|
||||
padding: 0px 20px;
|
||||
}
|
||||
|
||||
.totop {
|
||||
text-align: center;
|
||||
margin: 4em 10%;
|
||||
padding: 0.25em;
|
||||
background: #161616;
|
||||
}
|
||||
.totop a {
|
||||
margin-right: 7%;
|
||||
color: silver;
|
||||
}
|
||||
|
||||
|
||||
.block {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
#browserComp {
|
||||
border-collapse: collapse;
|
||||
margin: 40px auto;
|
||||
}
|
||||
#browserComp caption {
|
||||
text-align: center;
|
||||
margin: 20px auto;
|
||||
font-size: 1.1em;
|
||||
font-weight: bold;
|
||||
}
|
||||
#browserComp th {
|
||||
font-size: 10px;
|
||||
padding: 5px 10px;
|
||||
border-bottom: 1px solid silver;
|
||||
}
|
||||
#browserComp th img {
|
||||
width: 22px;
|
||||
}
|
||||
#browserComp td {
|
||||
text-align: center;
|
||||
}
|
||||
#browserComp th.feature,
|
||||
#browserComp td.feature {
|
||||
text-align: right;
|
||||
padding: 5px;
|
||||
padding-right: 10px;
|
||||
border-right: 1px solid silver;
|
||||
}
|
||||
|
||||
.addon {
|
||||
border: 1px solid #222;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
background: #222;
|
||||
}
|
||||
.addon .flowBox {
|
||||
width: 500px;
|
||||
margin-right: 40px;
|
||||
float: left;
|
||||
}
|
||||
.addon .discription {
|
||||
margin-left: 540px;
|
||||
}
|
||||
.addon .title {
|
||||
line-height: 1.3em;
|
||||
}
|
||||
.addon .title h3 {
|
||||
margin: 0;
|
||||
display: inline;
|
||||
}
|
||||
.addon .title .by {
|
||||
margin-left: 2em;
|
||||
font-size: 0.8em;
|
||||
text-decoration: none;
|
||||
}
|
||||
.addon p {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.addon .comment {
|
||||
margin-top: 2em;
|
||||
font-style: italic;
|
||||
}
|
||||
.addon .download {
|
||||
font-size: 0.8em;
|
||||
float: right;
|
||||
margin: 0 0 2em 4em;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
.addon .download a {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
#info { /* for example.php */
|
||||
color: white;
|
||||
/*display: none;*/
|
||||
}
|
||||
|
||||
@@ -1,329 +0,0 @@
|
||||
__version_info__ = (1,1,1)
|
||||
__version__ = '1.1.1'
|
||||
|
||||
import requests
|
||||
import json
|
||||
import urllib
|
||||
import httplib
|
||||
from collections import defaultdict
|
||||
from coverart_browser_prefs import GSetting
|
||||
|
||||
api_uri = 'http://api.discogs.com'
|
||||
user_agent = None
|
||||
|
||||
class APIBase(object):
|
||||
def __init__(self):
|
||||
self._cached_response = None
|
||||
self._params = {}
|
||||
self._headers = { 'accept-encoding': 'gzip, deflate' }
|
||||
|
||||
def __str__(self):
|
||||
return '<%s "%s">' % (self.__class__.__name__, self._id)
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__().encode('utf-8')
|
||||
|
||||
def _check_user_agent(self):
|
||||
if 'user_agent' in globals() and user_agent is not None:
|
||||
self._headers['user-agent'] = user_agent
|
||||
return 'user-agent' in self._headers and self._headers.get('user-agent')
|
||||
|
||||
def _clear_cache(self):
|
||||
self._cached_response = None
|
||||
|
||||
@property
|
||||
def _response(self):
|
||||
if not self._cached_response:
|
||||
if not self._check_user_agent():
|
||||
raise DiscogsAPIError, 'Invalid or no User-Agent set'
|
||||
try:
|
||||
#gs = GSetting()
|
||||
#setting = gs.get_setting(gs.Path.PLUGIN)
|
||||
#type_val = setting[gs.PluginKey.PROXY_TYPE]
|
||||
#if type_val == 0:
|
||||
# type_name = 'http'
|
||||
#elif type_val == 1:
|
||||
# type_name = 'https'
|
||||
#elif type_val == 2:
|
||||
# type_name = 'ftp'
|
||||
|
||||
#proxy_name = setting[gs.PluginKey.PROXY_VALUE]
|
||||
proxydict = {'http':''}
|
||||
#proxydict[type_name] = proxy_name
|
||||
self._cached_response = requests.get(self._uri, params=self._params, headers=self._headers, proxies=proxydict)
|
||||
except:
|
||||
raise DiscogsAPIError, 'bad response'
|
||||
|
||||
return self._cached_response
|
||||
|
||||
@property
|
||||
def _uri_name(self):
|
||||
return self.__class__.__name__.lower()
|
||||
|
||||
@property
|
||||
def _uri(self):
|
||||
return '%s/%s/%s' % (api_uri, self._uri_name, urllib.quote(unicode(self._id).encode('utf-8')))
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
if self._response.content and self._response.status_code == 200:
|
||||
release_json = json.loads(self._response.content)
|
||||
return release_json.get('resp').get(self._uri_name)
|
||||
else:
|
||||
status_code = self._response.status_code
|
||||
raise DiscogsAPIError, '%s %s' % (status_code, httplib.responses[status_code])
|
||||
|
||||
class DiscogsAPIError(BaseException):
|
||||
pass
|
||||
|
||||
def _parse_credits(extraartists):
|
||||
"""
|
||||
Parse release and track level credits
|
||||
"""
|
||||
_credits = defaultdict(list)
|
||||
for artist in extraartists:
|
||||
role = artist.get('role')
|
||||
tracks = artist.get('tracks')
|
||||
|
||||
artist_dict = {'artists': Artist(artist['name'], anv=artist.get('anv'))}
|
||||
|
||||
if tracks:
|
||||
artist_dict['tracks'] = tracks
|
||||
|
||||
_credits[role].append(artist_dict)
|
||||
return _credits
|
||||
|
||||
def _class_from_string(api_string):
|
||||
class_map = {
|
||||
'master': MasterRelease,
|
||||
'release': Release,
|
||||
'artist': Artist,
|
||||
'label': Label
|
||||
}
|
||||
|
||||
return class_map[api_string]
|
||||
|
||||
class Artist(APIBase):
|
||||
def __init__(self, name, anv=None):
|
||||
self._id = name
|
||||
self._aliases = []
|
||||
self._namevariations = []
|
||||
self._releases = []
|
||||
self._anv = anv or None
|
||||
APIBase.__init__(self)
|
||||
|
||||
def __str__(self):
|
||||
return '<%s "%s">' % (self.__class__.__name__, self._anv + '*' if self._anv else self._id)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._id
|
||||
|
||||
@property
|
||||
def anv(self):
|
||||
return self._anv
|
||||
|
||||
@property
|
||||
def aliases(self):
|
||||
if not self._aliases:
|
||||
for alias in self.data.get('aliases', []):
|
||||
self._aliases.append(Artist(alias))
|
||||
return self._aliases
|
||||
|
||||
@property
|
||||
def releases(self):
|
||||
# TODO: Implement fetch many release IDs
|
||||
#return [Release(r.get('id') for r in self.data.get('releases')]
|
||||
if not self._releases:
|
||||
self._params.update({'releases': '1'})
|
||||
self._clear_cache()
|
||||
|
||||
for r in self.data.get('releases', []):
|
||||
self._releases.append(_class_from_string(r['type'])(r['id']))
|
||||
return self._releases
|
||||
|
||||
class Release(APIBase):
|
||||
def __init__(self, id):
|
||||
self._id = id
|
||||
self._artists = []
|
||||
self._master = None
|
||||
self._labels = []
|
||||
self._credits = None
|
||||
self._tracklist = []
|
||||
APIBase.__init__(self)
|
||||
|
||||
@property
|
||||
def artists(self):
|
||||
if not self._artists:
|
||||
self._artists = [Artist(a['name']) for a in self.data.get('artists', [])]
|
||||
return self._artists
|
||||
|
||||
@property
|
||||
def master(self):
|
||||
if not self._master and self.data.get('master_id'):
|
||||
self._master = MasterRelease(self.data.get('master_id'))
|
||||
return self._master
|
||||
|
||||
@property
|
||||
def labels(self):
|
||||
if not self._labels:
|
||||
self._labels = [Label(l['name']) for l in self.data.get('labels', [])]
|
||||
return self._labels
|
||||
|
||||
@property
|
||||
def credits(self):
|
||||
if not self._credits:
|
||||
self._credits = _parse_credits(self.data.get('extraartists', []))
|
||||
return self._credits
|
||||
|
||||
@property
|
||||
def tracklist(self):
|
||||
if not self._tracklist:
|
||||
for track in self.data.get('tracklist', []):
|
||||
artists = []
|
||||
track['extraartists'] = _parse_credits(track.get('extraartists', []))
|
||||
|
||||
for artist in track.get('artists', []):
|
||||
artists.append(Artist(artist['name'], anv=artist.get('anv')))
|
||||
|
||||
if artist['join']:
|
||||
artists.append(artist['join'])
|
||||
track['artists'] = artists
|
||||
track['type'] = 'Track' if track['position'] else 'Index Track'
|
||||
|
||||
self._tracklist.append(track)
|
||||
return self._tracklist
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
return self.data.get('title')
|
||||
|
||||
class MasterRelease(APIBase):
|
||||
def __init__(self, id):
|
||||
self._id = id
|
||||
self._key_release = None
|
||||
self._versions = []
|
||||
self._artists = []
|
||||
APIBase.__init__(self)
|
||||
|
||||
# Override class name introspection in BaseAPI since it would otherwise return "masterrelease"
|
||||
@property
|
||||
def _uri_name(self):
|
||||
return 'master'
|
||||
|
||||
@property
|
||||
def key_release(self):
|
||||
if not self._key_release:
|
||||
self._key_release = Release(self.data.get('main_release'))
|
||||
return self._key_release
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
return self.key_release.data.get('title')
|
||||
|
||||
@property
|
||||
def versions(self):
|
||||
if not self._versions:
|
||||
for version in self.data.get('versions', []):
|
||||
self._versions.append(Release(version.get('id')))
|
||||
return self._versions
|
||||
|
||||
@property
|
||||
def artists(self):
|
||||
if not self._artists:
|
||||
for artist in self.data.get('artists', []):
|
||||
self._artists.append(Artist(artist.get('name')))
|
||||
return self._artists
|
||||
|
||||
@property
|
||||
def tracklist(self):
|
||||
return self.key_release.tracklist
|
||||
|
||||
class Label(APIBase):
|
||||
def __init__(self, name):
|
||||
self._id = name
|
||||
self._sublabels = []
|
||||
self._parent_label = None
|
||||
APIBase.__init__(self)
|
||||
|
||||
@property
|
||||
def sublabels(self):
|
||||
if not self._sublabels:
|
||||
for sublabel in self.data.get('sublabels', []):
|
||||
self._sublabels.append(Label(sublabel))
|
||||
return self._sublabels
|
||||
|
||||
@property
|
||||
def parent_label(self):
|
||||
if not self._parent_label and self.data.get('parentLabel'):
|
||||
self._parent_label = Label(self.data.get('parentLabel'))
|
||||
return self._parent_label
|
||||
|
||||
@property
|
||||
def releases(self):
|
||||
self._params.update({'releases': '1'})
|
||||
self._clear_cache()
|
||||
return self.data.get('releases')
|
||||
|
||||
class Search(APIBase):
|
||||
def __init__(self, query, page=1):
|
||||
self._id = query
|
||||
self._results = {}
|
||||
self._exactresults = []
|
||||
self._page = page
|
||||
APIBase.__init__(self)
|
||||
self._params['q'] = self._id
|
||||
self._params['page'] = self._page
|
||||
|
||||
def _to_object(self, result):
|
||||
id = result['title']
|
||||
if result['type'] in ('master', 'release'):
|
||||
id = result['uri'].split('/')[-1]
|
||||
elif result['type'] == 'artist':
|
||||
return Artist(id, anv=result.get('anv'))
|
||||
return _class_from_string(result['type'])(id)
|
||||
|
||||
@property
|
||||
def _uri(self):
|
||||
return '%s/%s' % (api_uri, self._uri_name)
|
||||
|
||||
@property
|
||||
def exactresults(self):
|
||||
if not self.data:
|
||||
return []
|
||||
|
||||
if not self._exactresults:
|
||||
for result in self.data.get('exactresults', []):
|
||||
self._exactresults.append(self._to_object(result))
|
||||
return self._exactresults
|
||||
|
||||
def results(self, page=1):
|
||||
page_key = 'page%s' % page
|
||||
|
||||
if page != self._page:
|
||||
if page > self.pages:
|
||||
raise DiscogsAPIError, 'Page number exceeds maximum number of pages returned'
|
||||
self._params['page'] = page
|
||||
self._clear_cache()
|
||||
|
||||
if not self.data:
|
||||
return []
|
||||
|
||||
if page_key not in self._results:
|
||||
self._results[page_key] = []
|
||||
for result in self.data['searchresults']['results']:
|
||||
self._results[page_key].append(self._to_object(result))
|
||||
|
||||
return self._results[page_key]
|
||||
|
||||
@property
|
||||
def numresults(self):
|
||||
if not self.data:
|
||||
return 0
|
||||
return int(self.data['searchresults'].get('numResults', 0))
|
||||
|
||||
@property
|
||||
def pages(self):
|
||||
if not self.data:
|
||||
return 0
|
||||
return (self.numresults / 20) + 1
|
||||
@@ -1,6 +1,7 @@
|
||||
import gi
|
||||
from gi.repository import Pango
|
||||
from gi.repository import Gtk
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
|
||||
import logging
|
||||
|
||||
|
Depois Largura: | Altura: | Tamanho: 14 KiB |
|
Depois Largura: | Altura: | Tamanho: 9.6 KiB |
|
Depois Largura: | Altura: | Tamanho: 7.2 KiB |
|
Depois Largura: | Altura: | Tamanho: 8.5 KiB |
|
Depois Largura: | Altura: | Tamanho: 7.6 KiB |
|
Depois Largura: | Altura: | Tamanho: 9.0 KiB |
|
Antes Largura: | Altura: | Tamanho: 3.3 KiB |
@@ -0,0 +1,93 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
inkscape:export-ydpi="90"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-filename="/home/jrbastien/Dropbox/CoverArt/Issue_225/coverart_monochrome_icon5.png"
|
||||
sodipodi:docname="coverart_icon_monochrome.svg"
|
||||
inkscape:version="0.48.4 r9939"
|
||||
version="1.1"
|
||||
width="16"
|
||||
height="16"
|
||||
id="svg7384">
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1313"
|
||||
inkscape:window-height="744"
|
||||
id="base"
|
||||
showgrid="false"
|
||||
inkscape:zoom="22.627417"
|
||||
inkscape:cx="7.8121502"
|
||||
inkscape:cy="8.1026068"
|
||||
inkscape:window-x="53"
|
||||
inkscape:window-y="24"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg7384">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid2984" />
|
||||
</sodipodi:namedview>
|
||||
<defs
|
||||
id="defs13" />
|
||||
<metadata
|
||||
id="metadata90">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title>Gnome Symbolic Icon Theme</dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<title
|
||||
id="title9167">Gnome Symbolic Icon Theme</title>
|
||||
<path
|
||||
style="color:#000000;fill:#bebebe;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
d="M 0 1 L 0 15 L 16 15 L 16 1 L 0 1 z M 3 2 L 15 2 L 15 14 L 3 14 L 3 2 z "
|
||||
id="path3049" />
|
||||
<path
|
||||
id="path3145"
|
||||
style="color:#000000;fill:#bebebe;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
d="m 8.49454,6.9999998 c -0.1963888,0 -0.2708736,0.00458 -0.421926,0.075312 C 7.8492631,7.1799014 7.6954236,7.3410447 7.5904128,7.5573095 7.5112238,7.7203905 7.5,7.8010382 7.5,7.9941194 c 0,0.1930808 0.011222,0.2737261 0.090413,0.4368099 0.1405533,0.2894579 0.401828,0.4949454 0.7233017,0.557309 0.1058087,0.020535 0.330263,0.013347 0.4369949,-0.015065 C 8.9273383,8.9261648 9.1300999,8.8031091 9.2479793,8.6719249 9.3497026,8.558718 9.4316453,8.391306 9.4740114,8.235115 c 0.034652,-0.1277581 0.034652,-0.3693021 0,-0.4970598 C 9.393561,7.4414403 9.2029191,7.2229953 8.9315348,7.0903743 8.7670195,7.0099746 8.6978322,6.9999998 8.49454,6.9999998 z m 0.00546,-3.2619392 c 0.3435847,0 0.6929605,0.032658 1.0213496,0.1135478 1.5301191,0.3769006 2.7439931,1.5920313 3.1207921,3.1225655 0.161741,0.6569562 0.161741,1.3868971 0,2.0438611 -0.413142,1.678183 -1.83062,2.926869 -3.5463542,3.179339 -0.2588467,0.03813 -0.7345973,0.05226 -0.9646085,0.02839 C 7.0640041,12.114865 6.1115479,11.676536 5.4075798,10.919964 4.646015,10.101478 4.2443755,9.103245 4.2443755,7.9961045 4.2443755,7.312468 4.3736153,6.7297403 4.6699382,6.1225653 5.2189083,4.9976584 6.2624431,4.1511883 7.4786511,3.8516084 7.8070437,3.7707188 8.1564165,3.7380606 8.5000013,3.7380606 z m 0,-0.738061 c -0.2467741,0 -0.4911568,0.00498 -0.6525293,0.028388 C 6.7061092,3.193961 5.780077,3.6778426 4.9820172,4.4761218 4.1839582,5.2743991 3.700631,6.2015307 3.5351045,7.3432048 c -0.046806,0.3228327 -0.046806,0.9829673 0,1.3058001 0.1655265,1.1416745 0.6488537,2.0688051 1.4469127,2.8670811 0.855663,0.855896 1.9055518,1.365449 3.1207923,1.476123 0.2703983,0.02463 0.8115804,-0.0132 1.1064627,-0.05677 1.0961058,-0.161943 2.0226018,-0.633028 2.8087138,-1.419348 0.798063,-0.798276 1.28138,-1.7254067 1.446912,-2.8670813 0.0468,-0.3228328 0.0468,-0.9829675 0,-1.3058001 C 13.299366,6.2015304 12.816049,5.2743988 12.017986,4.4761215 11.219926,3.6778423 10.293899,3.1939607 9.1525306,3.0283866 8.991163,3.0049817 8.7467754,2.9999996 8.5000013,2.9999996 z" />
|
||||
<rect
|
||||
style="color:#000000;fill:#bebebe;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
id="rect3809"
|
||||
width="1.4146408"
|
||||
height="13.103647"
|
||||
x="-2.9486878"
|
||||
y="2.1104977"
|
||||
transform="matrix(0,-1,0.99999431,0.00337266,0,0)" />
|
||||
<rect
|
||||
style="color:#000000;fill:#bebebe;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
id="rect3809-0"
|
||||
width="1.1411272"
|
||||
height="13.103621"
|
||||
x="-14.135278"
|
||||
y="2.1503386"
|
||||
transform="matrix(0,-1,0.9999963,0.00272058,0,0)" />
|
||||
<rect
|
||||
style="color:#000000;fill:#bebebe;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
id="rect3809-0-7"
|
||||
width="1.1411272"
|
||||
height="13.103621"
|
||||
x="-15.180982"
|
||||
y="-14.649317"
|
||||
transform="matrix(-1,0,0.00272058,-0.9999963,0,0)" />
|
||||
</svg>
|
||||
|
Depois Largura: | Altura: | Tamanho: 5.6 KiB |
|
Antes Largura: | Altura: | Tamanho: 60 KiB |
|
Depois Largura: | Altura: | Tamanho: 3.7 KiB |
|
Depois Largura: | Altura: | Tamanho: 2.1 KiB |
|
Depois Largura: | Altura: | Tamanho: 2.0 KiB |
|
Depois Largura: | Altura: | Tamanho: 33 KiB |
|
Depois Largura: | Altura: | Tamanho: 1.8 KiB |
|
Depois Largura: | Altura: | Tamanho: 40 KiB |
|
Depois Largura: | Altura: | Tamanho: 15 KiB |
|
Depois Largura: | Altura: | Tamanho: 18 KiB |
|
Depois Largura: | Altura: | Tamanho: 3.4 KiB |
|
Depois Largura: | Altura: | Tamanho: 13 KiB |
|
Depois Largura: | Altura: | Tamanho: 16 KiB |
|
Depois Largura: | Altura: | Tamanho: 4.8 KiB |
|
Depois Largura: | Altura: | Tamanho: 1.3 KiB |
|
Depois Largura: | Altura: | Tamanho: 1.2 KiB |
|
Depois Largura: | Altura: | Tamanho: 54 KiB |
|
Depois Largura: | Altura: | Tamanho: 2.7 KiB |
|
Depois Largura: | Altura: | Tamanho: 68 KiB |
|
Depois Largura: | Altura: | Tamanho: 19 KiB |
|
Depois Largura: | Altura: | Tamanho: 3.8 KiB |
|
Depois Largura: | Altura: | Tamanho: 17 KiB |
|
Depois Largura: | Altura: | Tamanho: 22 KiB |
|
Depois Largura: | Altura: | Tamanho: 2.7 KiB |
|
Antes Largura: | Altura: | Tamanho: 68 KiB |
|
Antes Largura: | Altura: | Tamanho: 4.0 KiB |
|
Depois Largura: | Altura: | Tamanho: 3.9 KiB |