Commit 7cc0d90d authored by Anton Palgunov's avatar Anton Palgunov

Merge branch 'master' into shop

parents bad3f058 f5e6275f
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Creator: CorelDRAW 2019 (64-Bit) -->
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="254mm" height="254mm" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
viewBox="0 0 25400 25400"
xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<style type="text/css">
<![CDATA[
.fil5 {fill:#0F164A}
.fil0 {fill:#303354}
.fil1 {fill:#5E81FF}
.fil4 {fill:#7B879D}
.fil2 {fill:#EEF2F1}
.fil3 {fill:#F8FAF9}
]]>
</style>
</defs>
<g id="Слой_x0020_1">
<metadata id="CorelCorpID_0Corel-Layer"/>
<g id="_2892636538912">
<path class="fil0" d="M903.95 11787.51l10287.29 -10264.88c1050.69,-955.6 2351.91,-833.29 3066.92,-1.82l10487.15 10432.66c679.55,599.05 572.43,1688.25 -829.66,1706.39l-2415.18 -12.02 -3.76 7015.2c-2.32,1628.67 -1943.73,3355.2 -3787.82,3384.97l-2527.87 0 -10973.09 -10456.54 -2924.88 66.45c-713.82,-21.17 -1442.47,-971.82 -379.1,-1870.41z"/>
<path class="fil1" d="M4223.55 13621.7l6400.31 6083.69 1.15 4368.31 -2361.45 10.55c-2711.35,-31.49 -4025.21,-2459.28 -4022.56,-3570.92l-17.45 -6891.63z"/>
<path class="fil2" d="M13153.63 12991.72l2518.06 -2221.52 16.99 -3552.67c2362.12,573.04 3110.45,5568.37 996.57,7302.33 -2113.88,1733.96 -4580.46,2325.58 -7092.36,72.96 -2511.91,-2252.61 -1691.34,-6269.57 909.54,-7329.57l-11.85 3446.34 2663.05 2282.13z"/>
<path class="fil3" d="M10639.38 24064.5l4774.42 0 13.47 -8682.3c-1616.1,873.98 -3220.19,831.22 -4787.89,-13.36l0 8695.66z"/>
<path class="fil4" d="M10648.34 16514.39c1616.03,663.08 3206.78,643.98 4774.43,0l18.45 -1139.89c-1567.41,858.99 -3185.76,872.11 -4801.84,-5.65l8.96 1145.54z"/>
<circle class="fil5" cx="12973.89" cy="19256.26" r="626.31"/>
<circle class="fil5" cx="12961.1" cy="21578.27" r="666.31"/>
</g>
</g>
</svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<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"
width="33.750061mm"
height="33.750061mm"
viewBox="0 0 33.750061 33.750061"
version="1.1"
id="svg974">
<defs
id="defs968">
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath18689">
<rect
clip-path="none"
transform="rotate(45)"
ry="32.000008"
rx="32.000008"
y="123.9986"
x="486.03726"
height="362.94299"
width="362.94299"
id="rect18691"
style="display:inline;opacity:1;vector-effect:none;fill:#4a86cf;fill-opacity:1;stroke:none;stroke-width:26.0669;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath18689-3">
<rect
clip-path="none"
transform="rotate(45)"
ry="32.000008"
rx="32.000008"
y="123.9986"
x="486.03726"
height="362.94299"
width="362.94299"
id="rect18691-6"
style="display:inline;opacity:1;vector-effect:none;fill:#4a86cf;fill-opacity:1;stroke:none;stroke-width:26.0669;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new" />
</clipPath>
</defs>
<metadata
id="metadata971">
<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
id="layer1"
transform="translate(-61.819823,-103.94395)">
<g
transform="matrix(0.26367235,0,0,0.26367235,61.819823,-529.39703)"
style="display:inline;stroke-width:0.25;enable-background:new"
id="g1836">
<title
id="title1838">application-x-executable</title>
<g
transform="matrix(0.25,0,0,0.25,0,2295)"
id="g18818"
style="stroke-width:0.25">
<g
style="stroke-width:0.269963"
transform="matrix(0.92605186,0,0,0.92605186,18.930729,50.876335)"
id="g18590">
<g
style="stroke-width:0.269963"
id="g18681"
clip-path="url(#clipPath18689-3)">
<rect
style="opacity:1;vector-effect:none;fill:#3584e4;fill-opacity:1;stroke:none;stroke-width:8.22095;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal"
id="rect18571"
width="424"
height="424"
x="458.33722"
y="90.641701"
rx="10.092117"
ry="10.092117"
transform="matrix(0.60528171,0.60528171,-0.60528171,0.60528171,33.440632,99.073632)"
clip-path="none" />
<circle
style="opacity:1;vector-effect:none;fill:#f66151;fill-opacity:1;stroke:none;stroke-width:7.03712;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal"
id="path18706"
cx="0"
cy="0"
r="0"
transform="translate(0,-212)" />
<circle
style="opacity:1;vector-effect:none;fill:#f66151;fill-opacity:1;stroke:none;stroke-width:7.03712;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal"
id="path18708"
cx="0"
cy="0"
r="0"
transform="translate(0,-212)" />
<path
style="display:inline;opacity:1;vector-effect:none;fill:#98c1f1;fill-opacity:1;stroke:none;stroke-width:7.03712;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new"
d="m 408.91993,561.9183 -9.8861,29.82892 a 172.97099,172.97099 0 0 0 -1.42693,-0.0713 172.97099,172.97099 0 0 0 -23.92891,1.85189 l -13.80082,-28.50125 a 203.29325,203.29325 0 0 0 -29.40085,7.97217 l 2.28619,31.40474 a 172.97099,172.97099 0 0 0 -22.73152,11.31923 l -23.71796,-21.103 a 203.29325,203.29325 0 0 0 -24.05918,18.6741 l 14.09863,28.07319 a 172.97099,172.97099 0 0 0 -16.63608,19.21074 l -30.05845,-10.44758 a 203.29325,203.29325 0 0 0 -15.01683,26.48807 l 23.73035,20.50738 a 172.97099,172.97099 0 0 0 -7.98456,24.14293 l -31.73044,1.84879 a 203.29325,203.29325 0 0 0 -3.77825,30.21664 l 29.82892,9.8861 a 172.97099,172.97099 0 0 0 -0.0713,1.42693 172.97099,172.97099 0 0 0 1.85188,23.92889 l -28.50125,13.80084 a 203.29325,203.29325 0 0 0 7.97215,29.40084 l 31.40475,-2.28619 a 172.97099,172.97099 0 0 0 11.31922,22.73152 l -21.10296,23.71797 a 203.29325,203.29325 0 0 0 18.67409,24.05918 l 28.07319,-14.09863 a 172.97099,172.97099 0 0 0 19.21074,16.63606 l -10.44758,30.05847 a 203.29325,203.29325 0 0 0 26.48806,15.01683 l 20.50739,-23.73036 a 172.97099,172.97099 0 0 0 24.14293,7.98457 l 1.8488,31.73043 a 203.29325,203.29325 0 0 0 30.21666,3.77826 l 9.8861,-29.82892 a 172.97099,172.97099 0 0 0 1.42693,0.0713 172.97099,172.97099 0 0 0 23.9289,-1.85188 l 13.80084,28.50125 a 203.29325,203.29325 0 0 0 29.40084,-7.97217 l -2.2862,-31.40474 a 172.97099,172.97099 0 0 0 22.73153,-11.31922 l 23.71796,21.10297 A 203.29325,203.29325 0 0 0 532.96,916.00016 l -14.09864,-28.07319 a 172.97099,172.97099 0 0 0 16.63607,-19.21073 l 30.05846,10.44757 a 203.29325,203.29325 0 0 0 15.01683,-26.48807 l -23.73036,-20.50738 a 172.97099,172.97099 0 0 0 7.98457,-24.14293 l 31.73044,-1.84879 a 203.29325,203.29325 0 0 0 3.77825,-30.21667 l -29.82892,-9.8861 a 172.97099,172.97099 0 0 0 0.0713,-1.42692 172.97099,172.97099 0 0 0 -1.85189,-23.9289 l 28.50124,-13.80084 a 203.29325,203.29325 0 0 0 -7.97215,-29.40084 l -31.40474,2.2862 a 172.97099,172.97099 0 0 0 -11.31923,-22.73153 l 21.10297,-23.71797 a 203.29325,203.29325 0 0 0 -18.67409,-24.05918 l -28.07319,14.09863 a 172.97099,172.97099 0 0 0 -19.21074,-16.63606 l 10.44757,-30.05847 A 203.29325,203.29325 0 0 0 485.6357,581.68117 l -20.50738,23.73035 a 172.97099,172.97099 0 0 0 -24.14293,-7.98455 l -1.84879,-31.73044 a 203.29325,203.29325 0 0 0 -30.21667,-3.77826 z M 397.6069,637.72208 A 126.92605,126.92605 0 0 1 524.5318,764.64699 126.92605,126.92605 0 0 1 397.6069,891.57189 126.92605,126.92605 0 0 1 270.682,764.64699 126.92605,126.92605 0 0 1 397.6069,637.72208 Z"
id="path18717-4" />
<path
id="path18758"
d="m 51.748325,401.28402 -9.8861,29.82892 c -0.475543,-0.0257 -0.951191,-0.0495 -1.42693,-0.0713 -8.00956,0.0625 -16.005106,0.6813 -23.92891,1.85189 L 2.7055639,404.39228 c -9.9858697,1.91835 -19.8137359,4.58322 -29.4008489,7.97217 l 2.28619,31.40474 c -7.844275,3.21103 -15.441918,6.9943 -22.73152,11.31923 l -23.71796,-21.103 c -8.475372,5.61437 -16.517661,11.85658 -24.05918,18.6741 l 14.09863,28.07319 c -6.008901,5.98701 -11.569263,12.40791 -16.636077,19.21074 l -30.058438,-10.44758 c -5.66072,8.44155 -10.68041,17.29574 -15.01683,26.48807 l 23.73035,20.50738 c -3.25027,7.84084 -5.919,15.91028 -7.98456,24.14293 l -31.73044,1.84879 c -2.01308,9.96359 -3.27604,20.06413 -3.77825,30.21664 l 29.82892,9.8861 c -0.0257,0.47554 -0.0495,0.95119 -0.0713,1.42693 0.0625,8.00955 0.68129,16.00509 1.85188,23.92889 l -28.50125,13.80084 c 1.91835,9.98587 4.58321,19.81373 7.97215,29.40084 l 31.40475,-2.28619 c 3.21102,7.84427 6.99429,15.44192 11.31922,22.73152 l -21.10296,23.71797 c 5.61437,8.47537 11.85658,16.51766 18.67409,24.05918 l 28.073175,-14.09863 c 5.987006,6.00889 12.407911,11.56925 19.21074,16.63606 l -10.44758,30.05847 c 8.441549,5.66072 17.295731,10.68041 26.48806,15.01683 l 20.50739,-23.73036 c 7.840839,3.25027 15.910282,5.91901 24.142929,7.98457 l 1.8488,31.73043 c 9.9635962,2.01309 20.064147,3.27605 30.216661,3.77826 l 9.8861,-29.82892 c 0.475543,0.0257 0.951191,0.0495 1.42693,0.0713 8.009557,-0.0625 16.005099,-0.68129 23.9289,-1.85188 l 13.80084,28.50125 c 9.985867,-1.91835 19.813731,-4.58322 29.400855,-7.97217 l -2.2862,-31.40474 c 7.84428,-3.21102 15.44192,-6.99429 22.73153,-11.31922 l 23.71796,21.10297 c 8.47538,-5.61437 16.51767,-11.85658 24.05919,-18.6741 l -14.09864,-28.07319 c 6.0089,-5.987 11.56926,-12.4079 16.63607,-19.21073 l 30.05846,10.44757 c 5.66072,-8.44155 10.68041,-17.29574 15.01683,-26.48807 l -23.73036,-20.50738 c 3.25027,-7.84084 5.91901,-15.91028 7.98457,-24.14293 l 31.73044,-1.84879 c 2.01308,-9.9636 3.27604,-20.06415 3.77825,-30.21667 l -29.82892,-9.8861 c 0.0257,-0.47554 0.0495,-0.95118 0.0713,-1.42692 -0.0625,-8.00956 -0.6813,-16.0051 -1.85189,-23.9289 l 28.50124,-13.80084 c -1.91835,-9.98587 -4.58321,-19.81373 -7.97215,-29.40084 l -31.40474,2.2862 c -3.21103,-7.84428 -6.9943,-15.44192 -11.31923,-22.73153 l 21.10297,-23.71797 c -5.61437,-8.47537 -11.85658,-16.51766 -18.67409,-24.05918 l -28.07319,14.09863 c -5.98701,-6.00889 -12.40791,-11.56925 -19.21074,-16.63606 l 10.44757,-30.05847 c -8.44155,-5.66072 -17.29572,-10.6804 -26.48805,-15.01682 l -20.50738,23.73035 c -7.84085,-3.25027 -15.910298,-5.91899 -24.142945,-7.98455 l -1.84879,-31.73044 c -9.9636,-2.01309 -20.064152,-3.27605 -30.21667,-3.77826 z"
style="display:inline;opacity:1;vector-effect:none;fill:#1a5fb4;fill-opacity:1;stroke:none;stroke-width:7.03712;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new" />
</g>
</g>
<path
style="display:inline;opacity:0.534;vector-effect:none;fill:#1a5fb4;fill-opacity:1;stroke:none;stroke-width:1.62918;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new"
clip-path="none"
d="m 8.4765625,2676 c -1.1711695,2.8866 -0.5827763,6.3078 1.7656255,8.6562 l 48.101562,48.1016 c 3.133898,3.1339 8.178602,3.1339 11.3125,0 l 48.10156,-48.1016 c 2.3484,-2.3484 2.9368,-5.7696 1.76563,-8.6562 -0.39174,0.9655 -0.98013,1.8708 -1.76563,2.6562 l -48.10156,48.1016 c -3.133898,3.1339 -8.178602,3.1339 -11.3125,0 L 10.242188,2678.6562 C 9.4566904,2677.8708 8.8682972,2676.9655 8.4765625,2676 Z"
transform="matrix(4,0,0,4,0,-10028)"
id="rect18571-6" />
</g>
<rect
y="2402"
x="-1.5000001e-06"
height="128"
width="128"
id="rect9125-7-2"
style="display:inline;opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:none;stroke-width:1.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new" />
</g>
</g>
</svg>
application_id = 'ru.ximperlinux.TuteIt'
application_id = 'ru.ximperlinux.TuneIt'
scalable_dir = 'hicolor' / 'scalable' / 'apps'
install_data(
......
desktop_file = i18n.merge_file(
input: 'ru.ximperlinux.TuteIt.desktop.in',
output: 'ru.ximperlinux.TuteIt.desktop',
input: 'ru.ximperlinux.TuneIt.desktop.in',
output: 'ru.ximperlinux.TuneIt.desktop',
type: 'desktop',
po_dir: '../po',
install: true,
......@@ -13,8 +13,8 @@ if desktop_utils.found()
endif
appstream_file = i18n.merge_file(
input: 'ru.ximperlinux.TuteIt.metainfo.xml.in',
output: 'ru.ximperlinux.TuteIt.metainfo.xml',
input: 'ru.ximperlinux.TuneIt.metainfo.xml.in',
output: 'ru.ximperlinux.TuneIt.metainfo.xml',
po_dir: '../po',
install: true,
install_dir: get_option('datadir') / 'metainfo'
......@@ -24,7 +24,7 @@ appstreamcli = find_program('appstreamcli', required: false, disabler: true)
test('Validate appstream file', appstreamcli,
args: ['validate', '--no-net', '--explain', appstream_file])
install_data('ru.ximperlinux.TuteIt.gschema.xml',
install_data('ru.ximperlinux.TuneIt.gschema.xml',
install_dir: get_option('datadir') / 'glib-2.0' / 'schemas'
)
......@@ -37,10 +37,22 @@ test('Validate schema file',
service_conf = configuration_data()
service_conf.set('bindir', get_option('prefix') / get_option('bindir'))
configure_file(
input: 'ru.ximperlinux.TuteIt.service.in',
output: 'ru.ximperlinux.TuteIt.service',
input: 'ru.ximperlinux.TuneIt.service.in',
output: 'ru.ximperlinux.TuneIt.service',
configuration: service_conf,
install_dir: get_option('datadir') / 'dbus-1' / 'services'
)
install_data('ru.ximperlinux.TuneIt.Daemon.policy',
install_dir: get_option('datadir') / 'polkit-1' / 'actions'
)
install_data('ru.ximperlinux.TuneIt.Daemon.conf',
install_dir: get_option('sysconfdir') / 'dbus-1' / 'system.d'
)
install_data('tuneit-daemon.service',
install_dir: get_option('systemd_unitdir')
)
subdir('icons')
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<type>system</type>
<!-- Только root может владеть сервисом -->
<policy user="root">
<allow own="ru.ximperlinux.TuneIt.Daemon"/>
<allow send_destination="ru.ximperlinux.TuneIt.Daemon"/>
<allow send_interface="ru.ximperlinux.TuneIt.DaemonInterface"/>
</policy>
<!-- Остальные пользователи могут вызывать методы -->
<policy context="default">
<allow send_destination="ru.ximperlinux.TuneIt.Daemon"/>
<allow send_interface="ru.ximperlinux.TuneIt.DaemonInterface"/>
</policy>
</busconfig>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policyconfig PUBLIC
"-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
"http://www.freedesktop.org/standards/PolicyKit/1.0/policyconfig.dtd">
<policyconfig><vendor>Example</vendor>
<vendor_url>https://example.com/example</vendor_url><action id="ru.ximperlinux.TuneIt.Daemon.auth">
<description gettext-domain="systemd">Authorization</description>
<message gettext-domain="tuneit">You need root rights to read and modify system configs.</message>
<defaults>
<!--These describe the auth level needed to do this.
Auth_admin, the current one, requires admin authentication every time.
Auth_admin_keep behaves like sudo, saving the password for a few minutes.Allow_inactive allows it to be accessed from SSH etc. Allow_active allows it to be accessed from the desktop.
Allow_any is a combo of both.
-->
<allow_any>auth_admin_keep</allow_any>
<allow_inactive>auth_admin_keep</allow_inactive>
<allow_active>auth_admin_keep</allow_active>
</defaults>
</action>
</policyconfig>
[Desktop Entry]
Name=tuneit
Exec=tuneit
Icon=ru.ximperlinux.TuteIt
Icon=ru.ximperlinux.TuneIt
Terminal=false
Type=Application
Categories=Utility;
......
<?xml version="1.0" encoding="UTF-8"?>
<schemalist gettext-domain="tuneit">
<schema id="ru.ximperlinux.TuteIt" path="/ru/ximperlinux/TuteIt/">
<schema id="ru.ximperlinux.TuneIt" path="/ru.ximperlinux.TuneIt/">
<key name="show_root_modules" type="b">
<default>false</default>
<description>Show modules that require root permissions</description>
</key>
</schema>
</schemalist>
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>ru.ximperlinux.TuteIt</id>
<id>ru.ximperlinux.TuneIt</id>
<metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0-or-later</project_license>
......@@ -36,7 +36,7 @@
<translation type="gettext">tuneit</translation>
<!-- All graphical applications having a desktop file must have this tag in the MetaInfo.
If this is present, appstreamcli compose will pull icons, keywords and categories from the desktop file. -->
<launchable type="desktop-id">ru.ximperlinux.TuteIt.desktop</launchable>
<launchable type="desktop-id">ru.ximperlinux.TuneIt.desktop</launchable>
<!-- Use the OARS website (https://hughsie.github.io/oars/generate.html) to generate these and make sure to use oars-1.1 -->
<content_rating type="oars-1.1" />
......
[D-BUS Service]
Name=ru.ximperlinux.TuteIt
Name=ru.ximperlinux.TuneIt
Exec=@bindir@/tuneit --gapplication-service
[Unit]
Description=TuneIt Daemon Service
After=network.target dbus.service
[Service]
Type=simple
User=root
ExecStart=tuneit-daemon
Restart=always
RestartSec=5
Environment=PYTHONUNBUFFERED=1
[Install]
WantedBy=multi-user.target
project('tuneit',
version: '0.1.0',
meson_version: '>= 1.0.0',
meson_version: '>= 1.2.0',
default_options: [ 'warning_level=2', 'werror=false', ],
)
......
option('systemd_unitdir', type: 'string', value: '/usr/lib/systemd/system', description: 'Path to systemd unit directory')
\ No newline at end of file
#!/bin/bash
sslk() {
gsettings set org.gnome.desktop.wm.keybindings switch-input-source "$1"
}
ck() {
gsettings set org.gnome.desktop.wm.keybindings switch-input-source "['']"
gsettings set org.gnome.desktop.wm.keybindings switch-input-source-backward "['']"
sslk "['']"
gsettings set org.gnome.desktop.input-sources xkb-options "['']"
}
get_range() {
echo '["<Ctrl>Shift_L", "<Super>space", "<Alt>Shift_L", "CapsLock"]'
}
set_value() {
ck
layout="$1"
case "$layout" in
"<Alt>Shift_L")
sslk "['$layout', '<Shift>Alt_L']"
;;
"<Ctrl>Shift_L")
sslk "['<Shift>Control_L', '<Ctrl>Shift_L']"
;;
"CapsLock")
gsettings set org.gnome.desktop.input-sources xkb-options "['grp:caps_toggle']"
;;
*)
sslk "['$layout']"
;;
esac
}
get_value() {
current_keybinding=$(gsettings get org.gnome.desktop.wm.keybindings switch-input-source)
current_xkb_options=$(gsettings get org.gnome.desktop.input-sources xkb-options)
if [[ "$current_xkb_options" == *"grp:caps_toggle"* ]]; then
echo "CapsLock"
else
case "$current_keybinding" in
"['<Ctrl>Shift_L', '<Shift>Control_L']")
echo "<Ctrl>Shift_L"
;;
"['<Alt>Shift_L', '<Shift>Alt_L']")
echo "<Alt>Shift_L"
;;
"['<Super>space']")
echo "<Super>space"
;;
"[]")
echo "No keybinding set"
;;
*)
echo "Unknown layout"
;;
esac
fi
}
command="$1"
case "$command" in
"get_range")
get_range
;;
"set_value")
layout="$2"
set_value "$layout"
;;
"get_value")
get_value
;;
*)
echo "Неизвестная команда: $command"
exit 1
;;
esac
- name: "Example"
- name: "Gnome"
weight: 30
pages:
- name: "Appearance"
icon: preferences-desktop-display-symbolic
- name: "Date & Time"
icon: preferences-system-time-symbolic
- name: "Power"
icon: battery-symbolic
- name: "Keyboard"
icon: input-keyboard-symbolic
- name: "System"
icon: preferences-system-symbolic
- name: "Boot"
icon: preferences-desktop-display-symbolic
- name: "Fonts"
icon: preferences-desktop-display-symbolic
sections:
- name: Themes
type: classic
- name: "Lang"
weight: 0
page: "Keyboard"
settings:
- name: keyboard shortcut
type: choice
gtype: string
backend: binary
params:
binary_path: "bin/"
binary_name: "langswitch.sh"
- name: "Themes"
weight: 0
page: "Appearance"
settings:
- name: IconTheme
type: entry
gtype: string
backend: gsettings
key: org.gnome.desktop.interface.icon-theme
default: "Adwaita"
- name: Style
type: choice
gtype: string
......@@ -26,13 +53,29 @@
gtype: string
backend: gsettings
key: org.gnome.desktop.background.picture-options
- name: Clock
- name: Wallpaper
type: file
gtype: string
backend: gsettings
key: org.gnome.desktop.background.picture-uri
map:
extensions: ["*.png", "*.jpeg", "*.jpg", "*.svg", "*.webp"]
- name: Wallpaper (dark)
type: file
gtype: string
backend: gsettings
key: org.gnome.desktop.background.picture-uri-dark
map:
extensions: [ "*.png", "*.jpeg", "*.jpg", "*.svg", "*.webp" ]
- name: "Clock"
weight: 10
page: "Date & Time"
settings:
- name: Weekday
type: boolean
gtype: boolean
backend: gsettings
default: true
key: org.gnome.desktop.interface.clock-show-weekday
- name: Date
type: boolean
......@@ -44,16 +87,18 @@
gtype: boolean
backend: gsettings
key: org.gnome.desktop.interface.clock-show-seconds
- name: Battery
- name: "Battery"
weight: 10
page: "Power"
settings:
- name: Show percentage
type: boolean
gtype: boolean
backend: gsettings
key: org.gnome.desktop.interface.show-battery-percentage
- name: Performance
- name: "Performance"
weight: 20
page: "System"
settings:
- name: Animations
help: Animations can be disabled for performance
......@@ -61,12 +106,9 @@
gtype: boolean
backend: gsettings
key: org.gnome.desktop.interface.enable-animations
- name: "Fonts"
weight: 1
sections:
- name: Main
type: classic
- name: "Main"
weight: 0
page: "Fonts"
settings:
- name: Antialiasing
type: choice
......@@ -80,7 +122,7 @@
Standard (grayscale): grayscale
None: none
- name: Hinting
type: choice
type: choice_radio
gtype: string
help:
backend: gsettings
......@@ -96,122 +138,110 @@
gtype: d
backend: gsettings
key: org.gnome.desktop.interface.text-scaling-factor
default: 1.0
map:
upper: 3.0
lower: 0.5
step: 0.01
digits: 2
- name: "Example2"
weight: 30
sections:
- name: Themes2
type: classic
weight: 0
settings:
- name: IconTheme
type: entry
gtype: string
backend: gsettings
key: org.gnome.desktop.interface.icon-theme
- name: Style
type: choice
gtype: string
help: Prefer dark or light for Adwaita applications
backend: gsettings
key: org.gnome.desktop.interface.color-scheme
default: "default"
map:
Default: default
Light: prefer-light
Dark: prefer-dark
- name: Clock
weight: 10
settings:
- name: Weekday
type: boolean
gtype: boolean
backend: gsettings
key: org.gnome.desktop.interface.clock-show-weekday
- name: Date
type: boolean
gtype: boolean
backend: gsettings
key: org.gnome.desktop.interface.clock-show-date
- name: Seconds
type: boolean
gtype: boolean
backend: gsettings
key: org.gnome.desktop.interface.clock-show-seconds
- name: Battery
weight: 10
settings:
- name: Show percentage
type: boolean
gtype: boolean
backend: gsettings
key: org.gnome.desktop.interface.show-battery-percentage
- name: Performance
weight: 20
settings:
- name: Animations
help: Animations can be disabled for performance
type: boolean
gtype: boolean
backend: gsettings
key: org.gnome.desktop.interface.enable-animations
- name: "ThemeSwitcher"
weight: 25
sections:
- name: Themes
type: classic
- name: "Main"
weight: 0
page: "Boot"
settings:
- name: KV Light Theme
type: entry
gtype: string
backend: file
key: KV_LIGHT_THEME
help: Select the Kvantum light theme
default: KvLibadwaita
params:
file_path: "~/.config/ximper-unified-theme-switcher/themes"
- name: KV Dark Theme
type: entry
gtype: string
backend: file
key: KV_DARK_THEME
help: Select the Kvantum dark theme
default: KvLibadwaitaDark
params:
file_path: "~/.config/ximper-unified-theme-switcher/themes"
- name: GTK3 Light Theme
type: entry
gtype: string
backend: file
key: GTK3_LIGHT_THEME
help: Select the GTK3 light theme
default: adw-gtk3
params:
file_path: "~/.config/ximper-unified-theme-switcher/themes"
- name: GTK3 Dark Theme
type: entry
gtype: string
- name: Timeout
root: true
type: number
gtype: i
backend: file
key: GTK3_DARK_THEME
help: Select the GTK3 dark theme
default: adw-gtk3-dark
key: GRUB_TIMEOUT
help: Select the GRUB2 timeout
params:
file_path: "~/.config/ximper-unified-theme-switcher/themes"
- name: Current Theme
type: choice
gtype: string
backend: file
key: CURRENT_THEME
help: Define the current theme preference
default: "prefer-dark"
file_path: "/etc/sysconfig/grub2"
default: 5
map:
Prefer Dark: prefer-dark
Prefer Light: prefer-light
Default: default
params:
file_path: "~/.config/ximper-unified-theme-switcher/themes"
upper: 999
lower: 0
step: 1
digits: 0
- name: "Main"
weight: 0
page: "Dirs"
settings:
- name: Templates
type: file
gtype: s
backend: file
key: XDG_TEMPLATES_DIR
help: Select templates folder
params:
file_path: "~/.config/user-dirs.dirs"
map:
extensions: folder
- name: Public Share
type: file
gtype: s
backend: file
key: XDG_PUBLICSHARE_DIR
help: Select public share folder
params:
file_path: "~/.config/user-dirs.dirs"
map:
extensions: folder
- name: Documents
type: file
gtype: s
backend: file
key: XDG_DOCUMENTS_DIR
help: Select documents folder
default: "~/Документы"
params:
file_path: "~/.config/user-dirs.dirs"
map:
extensions: folder
- name: Music
type: file
gtype: s
backend: file
key: XDG_MUSIC_DIR
help: Select music folder
params:
file_path: "~/.config/user-dirs.dirs"
map:
extensions: folder
- name: Pictures
type: file
gtype: s
backend: file
key: XDG_PICTURES_DIR
help: Select pictures folder
params:
file_path: "~/.config/user-dirs.dirs"
map:
extensions: folder
- name: Videos
type: file
gtype: s
backend: file
key: XDG_VIDEOS_DIR
help: Select videos folder
params:
file_path: "~/.config/user-dirs.dirs"
map:
extensions: folder
- name: Downloads
type: file
gtype: s
backend: file
key: XDG_DOWNLOAD_DIR
help: Select downloads folder
default: "~/Загрузки"
params:
file_path: "~/.config/user-dirs.dirs"
map:
extensions: folder
\ No newline at end of file
pluginsdir = pkgdatadir / 'modules'
modules_dir = pkgdatadir / 'modules'
plugins = [
'exampleplug.yml',
'example_module',
]
install_data(plugins, install_dir: pluginsdir)
install_subdir(plugins, install_dir: modules_dir)
# Please keep this file sorted alphabetically.
ru_RU
\ No newline at end of file
# List of source files containing translatable strings.
# Please keep this file sorted alphabetically.
data/ru.ximperlinux.TuteIt.desktop.in
data/ru.ximperlinux.TuteIt.metainfo.xml.in
data/ru.ximperlinux.TuteIt.gschema.xml
data/ru.ximperlinux.TuneIt.desktop.in
data/ru.ximperlinux.TuneIt.metainfo.xml.in
data/ru.ximperlinux.TuneIt.gschema.xml
data/ru.ximperlinux.TuneIt.Daemon.policy
src/main.py
src/window.py
src/window.ui
src/window.blp
src/settings/main.py
src/settings/widgets/service_dialog.py
\ No newline at end of file
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the tuneit package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: tuneit\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-21 13:16+0300\n"
"PO-Revision-Date: 2025-01-21 13:18+0300\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: ru_RU\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 3.5\n"
#: data/ru.ximperlinux.TuneIt.desktop.in:3
msgid "tuneit"
msgstr ""
#: data/ru.ximperlinux.TuneIt.desktop.in:9
msgid "GTK;"
msgstr ""
#: data/ru.ximperlinux.TuneIt.metainfo.xml.in:7 src/window.blp:92
msgid "TuneIt"
msgstr ""
#: data/ru.ximperlinux.TuneIt.metainfo.xml.in:8
msgid "Keep the summary shorter, between 10 and 35 characters"
msgstr ""
#: data/ru.ximperlinux.TuneIt.metainfo.xml.in:10
msgid "No description"
msgstr ""
#: data/ru.ximperlinux.TuneIt.metainfo.xml.in:52
#: data/ru.ximperlinux.TuneIt.metainfo.xml.in:56
msgid "A caption"
msgstr ""
#: data/ru.ximperlinux.TuneIt.gschema.xml:6
msgid "Show modules that require root permissions"
msgstr "Показывать модули которым нужны root права"
#. Translators: Replace "translator-credits" with your name/username, and optionally an email or URL.
#: src/main.py:65
msgid "translator-credits"
msgstr "ximper@etersoft.ru"
#: src/window.blp:26
msgid "Main Menu"
msgstr ""
#: src/window.blp:72
msgid "Settings"
msgstr "Настройки"
#: src/window.blp:79
msgid "Shop"
msgstr "Магазин"
#: src/window.blp:99
msgid "_Preferences"
msgstr ""
#: src/window.blp:104
msgid "_Keyboard Shortcuts"
msgstr ""
#: src/window.blp:109
msgid "_About TuneIt"
msgstr "О программе"
#: src/settings/widgets/service_dialog.py:13
msgid "Dbus service is disabled or unresponsive."
msgstr "Dbus сервис отключен или не отвечает."
#: src/settings/widgets/service_dialog.py:14
msgid ""
"It is needed for modules that require root permissions.\n"
"Do you want to try to turn on the service?\n"
"Tune It will restart after enabling the service."
msgstr ""
"Он нужен для модулей, которым необходимы права root.\n"
"Вы хотите попробовать включить сервис?\n"
"После включения сервиса Tune It перезапустится."
#: src/settings/widgets/service_dialog.py:16
msgid "Yes"
msgstr "Да"
#: src/settings/widgets/service_dialog.py:17
msgid "No"
msgstr "Нет"
#~ msgid "You need root rights to read and modify system configs."
#~ msgstr "Вам нужны права суперпользователя (root), чтобы читать и изменять конфигм системы."
{
"id" : "ru.ximperlinux.TuteIt",
"id" : "ru.ximperlinux.TuneIt",
"runtime" : "org.gnome.Platform",
"runtime-version" : "47",
"sdk" : "org.gnome.Sdk",
......
#!@PYTHON@
# tuneit.in
#
# Copyright 2024 Unknown
#
# 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 3 of the License, 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, see <https://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later
import os
import sys
import signal
import locale
import gettext
VERSION = '@VERSION@'
pkgdatadir = '@pkgdatadir@'
localedir = '@localedir@'
sys.path.insert(1, pkgdatadir)
signal.signal(signal.SIGINT, signal.SIG_DFL)
locale.bindtextdomain('tuneit', localedir)
locale.textdomain('tuneit')
gettext.install('tuneit', localedir)
if __name__ == '__main__':
import gi
from gi.repository import Gio
resource = Gio.Resource.load(os.path.join(pkgdatadir, 'tuneit.gresource'))
resource._register()
from tuneit import daemon
sys.exit(daemon.main())
import ast
import logging
import dbus
import dbus.mainloop.glib
import dbus.service
from gi.repository import GLib
from .settings.backends import root_backend_factory
# Настройка логирования
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)
class Daemon(dbus.service.Object):
def __init__(self, bus, object_path):
super().__init__(bus, object_path)
self.dbus_info = None
self.polkit = None
def _check_polkit_privilege(self, sender, conn, privilege):
"""
Проверяет привилегии с использованием PolicyKit.
:param sender: Отправитель D-Bus сообщения.
:param conn: Соединение D-Bus.
:param privilege: Проверяемая привилегия.
:return: True, если авторизация успешна; иначе False.
"""
if self.dbus_info is None:
self.dbus_info = dbus.Interface(
conn.get_object("org.freedesktop.DBus", "/org/freedesktop/DBus"),
"org.freedesktop.DBus"
)
pid = self.dbus_info.GetConnectionUnixProcessID(sender)
if self.polkit is None:
try:
bus_object = dbus.SystemBus().get_object(
"org.freedesktop.PolicyKit1",
"/org/freedesktop/PolicyKit1/Authority"
)
self.polkit = dbus.Interface(bus_object, "org.freedesktop.PolicyKit1.Authority")
except Exception as e:
logger.error(f"Failed to connect to PolicyKit: {e}")
raise
retry_limit = 3
retry_count = 0
while retry_count < retry_limit:
try:
auth_response = self.polkit.CheckAuthorization(
("unix-process", {"pid": dbus.UInt32(pid, variant_level=1),
"start-time": dbus.UInt64(0, variant_level=1)}),
privilege, {"AllowUserInteraction": "true"}, dbus.UInt32(1), "", timeout=600
)
is_auth, _, _ = auth_response
return is_auth
except dbus.DBusException as e:
if e._dbus_error_name == "org.freedesktop.DBus.Error.ServiceUnknown":
retry_count += 1
logger.warning(f"PolicyKit service unavailable, retrying ({retry_count}/{retry_limit})...")
self.polkit = None
else:
logger.error(f"DBusException occurred: {e}")
raise
logger.error("Failed to authorize: PolicyKit service unavailable after retries.")
return False
@dbus.service.method(
dbus_interface="ru.ximperlinux.TuneIt.DaemonInterface",
in_signature="ssss",
out_signature="s",
sender_keyword="sender",
connection_keyword="conn"
)
def GetValue(self, backend_name, backend_params, key, gtype, sender=None, conn=None):
if not self._check_polkit_privilege(sender, conn, "ru.ximperlinux.TuneIt.Daemon.auth"):
raise dbus.DBusException(
"org.freedesktop.DBus.Error.AccessDenied",
"Permission denied"
)
try:
backend_params = ast.literal_eval(backend_params)
backend = root_backend_factory.get_backend(backend_name, backend_params)
if backend:
return str(backend.get_value(key, gtype))
except Exception as e:
return dbus.DBusException(
"ru.ximperlinux.TuneIt.Daemon", e
)
return f"backend_name: {backend_name}, params: {backend_params}, key: {key}"
@dbus.service.method(
dbus_interface="ru.ximperlinux.TuneIt.DaemonInterface",
in_signature="sssss",
out_signature="s",
sender_keyword="sender",
connection_keyword="conn"
)
def SetValue(self, backend_name, backend_params, key, value, gtype, sender=None, conn=None):
if not self._check_polkit_privilege(sender, conn, "ru.ximperlinux.TuneIt.Daemon.auth"):
raise dbus.DBusException(
"org.freedesktop.DBus.Error.AccessDenied",
"Permission denied"
)
try:
backend_params = ast.literal_eval(backend_params)
backend = root_backend_factory.get_backend(backend_name, backend_params)
if backend:
backend.set_value(key, value, gtype)
except Exception as e:
return dbus.DBusException(
"ru.ximperlinux.TuneIt.Daemon", e
)
return f"Failed to set value for backend_name: {backend_name}, key: {key}"
@dbus.service.method(
dbus_interface="ru.ximperlinux.TuneIt.DaemonInterface",
in_signature="ssss",
out_signature="s",
sender_keyword="sender",
connection_keyword="conn"
)
def GetRange(self, backend_name, backend_params, key, gtype, sender=None, conn=None):
if not self._check_polkit_privilege(sender, conn, "ru.ximperlinux.TuneIt.Daemon.auth"):
raise dbus.DBusException(
"org.freedesktop.DBus.Error.AccessDenied",
"Permission denied"
)
try:
backend_params = ast.literal_eval(backend_params)
backend = root_backend_factory.get_backend(backend_name, backend_params)
if backend:
return str(backend.get_range(key, gtype))
except Exception as e:
return dbus.DBusException(
"ru.ximperlinux.TuneIt.Daemon", e
)
return f"Failed to get range for backend_name: {backend_name}, key: {key}"
def main():
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
try:
bus = dbus.SystemBus()
name = dbus.service.BusName("ru.ximperlinux.TuneIt.Daemon", bus)
daemon = Daemon(bus, "/Daemon")
logger.info("Service is running...")
mainloop = GLib.MainLoop()
mainloop.run()
except KeyboardInterrupt:
logger.info("Service interrupted by user.")
except Exception as e:
logger.error(f"Error: {e}")
if __name__ == "__main__":
main()
#!/usr/bin/bash
find . -type f -name "*.blp" | sed "s|^\./$1/||"
\ No newline at end of file
......@@ -26,17 +26,22 @@ gi.require_version('Adw', '1')
from gi.repository import Gtk, Gio, Adw
from .window import TuneitWindow
def get_main_window():
return _application.props.active_window
class TuneitApplication(Adw.Application):
"""The main application singleton class."""
def __init__(self):
super().__init__(application_id='ru.ximperlinux.TuteIt',
super().__init__(application_id='ru.ximperlinux.TuneIt',
flags=Gio.ApplicationFlags.DEFAULT_FLAGS)
global _application
self.create_action('quit', lambda *_: self.quit(), ['<primary>q'])
self.create_action('about', self.on_about_action)
self.create_action('preferences', self.on_preferences_action)
_application = self
def do_activate(self):
"""Called when the application is activated.
......@@ -51,7 +56,7 @@ class TuneitApplication(Adw.Application):
def on_about_action(self, *args):
"""Callback for the app.about action."""
about = Adw.AboutDialog(application_name='tuneit',
application_icon='ru.ximperlinux.TuteIt',
application_icon='ru.ximperlinux.TuneIt',
developer_name='Etersoft',
version='0.1.0',
developers=['Ximper'],
......
......@@ -2,9 +2,14 @@ pkgdatadir = get_option('prefix') / get_option('datadir') / meson.project_name()
moduledir = pkgdatadir / 'tuneit'
gnome = import('gnome')
blp_search = run_command('find_blp.sh', '', check: true)
blp_files = blp_search.stdout().splitlines()
blueprints = custom_target('blueprints',
input: files(
'window.blp',
blp_files,
),
output: '.',
command: [find_program('blueprint-compiler'), 'batch-compile', '@OUTPUT@', '@CURRENT_SOURCE_DIR@', '@INPUT@'],
......@@ -35,13 +40,30 @@ configure_file(
install_mode: 'r-xr-xr-x'
)
configure_file(
input: 'daemon.in',
output: 'tuneit-daemon',
configuration: conf,
install: true,
install_dir: get_option('sbindir'),
install_mode: 'r-xr-xr-x'
)
tuneit_sources = [
'__init__.py',
'main.py',
'window.py',
'daemon.py',
]
install_data(tuneit_sources, install_dir: moduledir)
install_subdir('settings', install_dir: moduledir, strip_directory : false)
install_subdir('shop', install_dir: moduledir, strip_directory : false)
\ No newline at end of file
blp_search_settings = run_command('find_blp.sh', 'settings', check: true)
blp_search_shop = run_command('find_blp.sh', 'shop', check: true)
blp_files_settings = blp_search_settings.stdout().splitlines()
blp_files_shop = blp_search_shop.stdout().splitlines()
install_subdir('settings', install_dir: moduledir, strip_directory : false, exclude_files: blp_files_settings)
# install_subdir('shop', install_dir: moduledir, strip_directory : false, exclude_files: blp_files_shop)
from gi.repository import Adw, Gtk
from .backends import backend_factory
from .tools.yml_tools import load_modules, merge_categories_by_name
from .widgets import WidgetFactory
class Setting:
def __init__(self, setting_data):
self.name = setting_data['name']
self.backend = setting_data.get('backend')
self.params = setting_data.get('params', {})
self.type = setting_data['type']
self.help = setting_data.get('help', "")
self.key = setting_data.get('key')
self.default = setting_data.get('default')
self.gtype = setting_data.get('gtype', [])
self.map = setting_data.get('map', self._default_map())
self.data = setting_data.get('data', {})
if len(self.gtype) > 2:
self.gtype = self.gtype[0]
else:
self.gtype = self.gtype
def _default_map(self):
if self.type == 'boolean':
# Дефолтная карта для булевых настроек
return {True: True, False: False}
if self.type == 'choice':
# Дефолтная карта для выборов
map = {}
range = self._get_backend_range()
if range is None:
return {}
for var in range:
print(var)
map[var[0].upper() + var[1:]] = var
return map
if self.type == 'number':
map = {}
range = self._get_backend_range()
if range is None:
return {}
map["upper"] = range[1]
map["lower"] = range[0]
# Кол-во после запятой
map["digits"] = len(str(range[0]).split('.')[-1]) if '.' in str(range[0]) else 0
# Минимальное число с этим количеством
map["step"] = 10 ** -map["digits"] if map["digits"] > 0 else 0
return map
return {}
def create_row(self):
widget = WidgetFactory.create_widget(self)
return widget.create_row() if widget else None
def _get_selected_row_index(self):
current_value = self._get_backend_value()
return list(self.map.values()).index(current_value) if current_value in self.map.values() else 0
def _get_backend_value(self):
backend = self._get_backend()
if backend:
return backend.get_value(self.key, self.gtype)
return self.default
def _get_backend_range(self):
backend = self._get_backend()
if backend:
return backend.get_range(self.key, self.gtype)
def _set_backend_value(self, value):
backend = self._get_backend()
if backend:
backend.set_value(self.key, value, self.gtype)
def _get_backend(self):
backend = backend_factory.get_backend(self.backend, self.params)
if not backend:
print(f"Бекенд {self.backend} не зарегистрирован.")
return backend
class Section:
def __init__(self, section_data, strategy):
self.name = section_data['name']
self.weight = section_data.get('weight', 0)
self.settings = [Setting(s) for s in section_data.get('settings', [])]
self.strategy = strategy
def create_preferences_group(self):
return self.strategy.create_preferences_group(self)
class SectionStrategy:
def create_preferences_group(self, section):
raise NotImplementedError("Метод create_preferences_group должен быть реализован")
class ClassicSectionStrategy(SectionStrategy):
def create_preferences_group(self, section):
group = Adw.PreferencesGroup(title=section.name)
for setting in section.settings:
row = setting.create_row()
if row:
print(f"Добавление строки для настройки: {setting.name}")
group.add(row)
else:
print(f"Не удалось создать строку для настройки: {setting.name}")
return group
class NewSectionStrategy(SectionStrategy):
def create_preferences_group(self, section):
group = Adw.PreferencesGroup(title=section.name)
print(f"Создание секции нового типа: {section.name}")
for setting in section.settings:
row = setting.create_row()
group.add(row)
return group
class SectionFactory:
def __init__(self):
self.strategies = {
'classic': ClassicSectionStrategy(),
}
def create_section(self, section_data):
section_type = section_data.get('type', 'classic')
strategy = self.strategies.get(section_type)
if not strategy:
raise ValueError(f"Неизвестный тип секции: {section_type}")
return Section(section_data, strategy)
class Category:
def __init__(self, category_data, section_factory: SectionFactory):
self.name = category_data['name']
self.weight = category_data.get('weight', 0)
self.sections = [section_factory.create_section(s) for s in category_data.get('sections', [])]
def create_stack_page(self, stack):
box = Gtk.ScrolledWindow()
pref_page = Adw.PreferencesPage()
clamp = Adw.Clamp()
clamp.set_child(pref_page)
box.set_child(clamp)
for section in self.sections:
preferences_group = section.create_preferences_group()
if preferences_group:
pref_page.add(preferences_group)
else:
print(f"Секция {section.name} не создала виджетов.")
stack_page = stack.add_child(box)
stack_page.set_title(self.name)
stack_page.set_name(self.name)
def init_settings_stack(stack, listbox, split_view):
yaml_data = load_modules()
merged_data = merge_categories_by_name(yaml_data)
section_factory = SectionFactory()
categories = [Category(c, section_factory) for c in merged_data]
for category in categories:
category.create_stack_page(stack)
if not stack:
print("Ошибка: settings_pagestack не найден.")
def populate_listbox_from_stack():
pages = stack.get_pages()
for i in range(pages.get_n_items()):
page = pages.get_item(i)
label = Gtk.Label(label=page.get_title(), xalign=0)
row = Gtk.ListBoxRow()
row.set_name(page.get_name())
row.set_child(label)
listbox.append(row)
def on_row_selected(listbox, row):
if row:
page_id = row.get_name()
print(f"Selected page: {page_id}")
visible_child = stack.get_child_by_name(page_id)
if visible_child:
stack.set_visible_child(visible_child)
split_view.set_show_content(True)
listbox.connect("row-selected", on_row_selected)
populate_listbox_from_stack()
from gi.repository import Gio, GLib
import json
import yaml
import os
from configparser import ConfigParser
class Backend:
def __init__(self, params=None):
# Параметры, передаваемые при инициализации
self.params = params or {}
def get_value(self, key, gtype):
raise NotImplementedError("Метод get_value должен быть реализован")
def get_range(self, key, gtype):
raise NotImplementedError("Метод get_range должен быть реализован")
def set_value(self, key, value, gtype):
raise NotImplementedError("Метод set_value должен быть реализован")
class GSettingsBackend(Backend):
def get_value(self, key, gtype):
schema_name, key_name = key.rsplit('.', 1)
schema = Gio.Settings.new(schema_name)
print(f"[DEBUG] Получение значения: schema={schema_name}, key={key_name}, gtype={gtype}")
try:
value = schema.get_value(key_name)
return value.unpack()
except Exception as e:
print(f"[ERROR] Ошибка при получении значения {key}: {e}")
return None
def get_range(self, key, gtype):
schema_name, key_name = key.rsplit('.', 1)
schema = Gio.Settings.new(schema_name)
print(f"[DEBUG] Получение значения: schema={schema_name}, key={key_name}, gtype={gtype}")
try:
value = schema.get_range(key_name)
return value.unpack()[1]
except Exception as e:
print(f"[ERROR] Ошибка при получении значения {key}: {e}")
return None
def set_value(self, schema_key, value, gtype):
schema_name, key_name = schema_key.rsplit('.', 1)
schema = Gio.Settings.new(schema_name)
print(f"[DEBUG] Установка значения: schema={schema_name}, key={key_name}, value={value}, gtype={gtype}")
try:
schema.set_value(key_name, GLib.Variant(gtype, value))
except Exception as e:
print(f"[ERROR] Ошибка при установке значения {schema_key}: {e}")
class FileBackend(Backend):
def __init__(self, params=None):
super().__init__(params)
self.file_path = os.path.expanduser(self.params.get('file_path'))
self.encoding = self.params.get('encoding', 'utf-8')
self.file_type = self._get_file_type()
def _get_file_type(self):
_, ext = os.path.splitext(self.file_path)
ext = ext.lower()
if ext == '.json':
return 'json'
elif ext == '.yaml' or ext == '.yml':
return 'yaml'
elif ext == '.ini':
return 'ini'
elif ext == '.sh' or ext == '.conf':
return 'text'
else:
return 'text'
def _read_file(self):
try:
with open(self.file_path, 'r', encoding=self.encoding) as file:
if self.file_type == 'json':
return json.load(file)
elif self.file_type == 'yaml':
return yaml.safe_load(file)
elif self.file_type == 'ini':
config = ConfigParser()
config.read_file(file)
return config
elif self.file_type == 'text':
return self._parse_text_config(file)
else:
raise ValueError(f"Unsupported file type: {self.file_type}")
except Exception as e:
print(f"[ERROR] Ошибка при чтении файла {self.file_path}: {e}")
return None
def _write_file(self, data):
try:
with open(self.file_path, 'w', encoding=self.encoding) as file:
if self.file_type == 'json':
json.dump(data, file, indent=4)
elif self.file_type == 'yaml':
yaml.dump(data, file, default_flow_style=False)
elif self.file_type == 'ini':
config = ConfigParser()
for section, values in data.items():
config[section] = values
config.write(file)
elif self.file_type == 'text':
self._write_text_config(file, data)
else:
raise ValueError(f"Unsupported file type: {self.file_type}")
except Exception as e:
print(f"[ERROR] Ошибка при записи в файл {self.file_path}: {e}")
def _parse_text_config(self, file):
config = {}
for line in file:
line = line.strip()
if not line or line.startswith('#'):
continue
if '=' in line:
key, value = line.split('=', 1)
config[key.strip()] = value.strip()
return config
def _write_text_config(self, file, data):
existing_lines = []
with open(self.file_path, 'r', encoding=self.encoding) as file_read:
existing_lines = file_read.readlines()
existing_style = self._detect_text_style(existing_lines)
for key, value in data.items():
if existing_style == 'space_around':
file.write(f"{key} = {value}\n")
elif existing_style == 'no_space':
file.write(f"{key}={value}\n")
else:
file.write(f"{key} = {value}\n")
def _detect_text_style(self, lines):
style = None
for line in lines:
line = line.strip()
if '=' in line:
if line.startswith(' ') and line.endswith(' '):
style = 'space_around'
break
elif line.find('=') == len(line.split('=')[0]):
style = 'no_space'
break
return style or 'space_around'
def get_value(self, key, gtype):
data = self._read_file()
if data is None:
return None
if self.file_type == 'json' or self.file_type == 'yaml':
return data.get(key, None)
elif self.file_type == 'ini':
section, key_name = key.split('.', 1)
if section in data:
return data[section].get(key_name, None)
elif self.file_type == 'text':
return data.get(key, None)
return None
def get_range(self, key, gtype):
data = self._read_file()
if data is None:
return None
if self.file_type == 'json' or self.file_type == 'yaml':
if isinstance(data.get(key), list):
return (min(data[key]), max(data[key]))
elif self.file_type == 'ini':
pass
return None
def set_value(self, key, value, gtype):
data = self._read_file()
if data is None:
return
if self.file_type == 'json' or self.file_type == 'yaml':
data[key] = value
elif self.file_type == 'ini':
section, key_name = key.split('.', 1)
if section not in data:
data[section] = {}
data[section][key_name] = value
elif self.file_type == 'text':
data[key] = value
self._write_file(data)
class BackendFactory:
def __init__(self):
self.backends = {
'gsettings': GSettingsBackend,
'file': FileBackend,
}
def get_backend(self, backend_name, params=None):
backend_class = self.backends.get(backend_name)
if backend_class:
# Передаем параметры в конструктор бэкенда, если они есть
return backend_class(params) if params else backend_class()
return None
backend_factory = BackendFactory()
from .base import Backend
from .gsettings import GSettingsBackend
from .file import FileBackend
from .binary import BinaryBackend
class BackendFactory:
def __init__(self):
self.backends = {
'gsettings': GSettingsBackend,
'file': FileBackend,
'binary': BinaryBackend,
}
def get_backend(self, backend_name, params=None):
backend_class = self.backends.get(backend_name)
if backend_class:
# Передаем параметры в конструктор бэкенда, если они есть
return backend_class(params) if params else backend_class()
return None
backend_factory = BackendFactory()
class RootBackendFactory:
def __init__(self):
self.backends = {
'file': FileBackend,
'binary': BinaryBackend,
}
def get_backend(self, backend_name, params=None):
backend_class = self.backends.get(backend_name)
if backend_class:
# Передаем параметры в конструктор бэкенда, если они есть
return backend_class(params) if params else backend_class()
return None
root_backend_factory = RootBackendFactory()
class Backend:
def __init__(self, params=None):
# Параметры, передаваемые при инициализации
self.params = params or {}
def get_value(self, key, gtype):
raise NotImplementedError("Метод get_value должен быть реализован")
def get_range(self, key, gtype):
raise NotImplementedError("Метод get_range должен быть реализован")
def set_value(self, key, value, gtype):
raise NotImplementedError("Метод set_value должен быть реализован")
import ast
import os
import subprocess
from .base import Backend
class BinaryBackend(Backend):
def __init__(self, params=None):
super().__init__(params)
self.binary_path = os.path.join(
self.params.get('module_path'),
self.params.get('binary_path')
)
self.binary_name = self.params.get('binary_name')
def _run_binary(self, command, *args):
try:
full_command = (
[self.binary_path + self.binary_name, command]
+ [x for x in args if x is not None]
)
result = subprocess.run(full_command, capture_output=True, text=True, check=True)
return result.stdout.strip()
except subprocess.CalledProcessError as e:
print(f"[ERROR] Ошибка при выполнении команды {command}: {e}")
return None
def get_value(self, key, gtype):
print(f"[DEBUG] Получение значения: key={key}, gtype={gtype}")
result = self._run_binary('get_value', key)
if result:
try:
return ast.literal_eval(result)
except (ValueError, SyntaxError) as e:
print(f"[ERROR] Ошибка при преобразовании результата {result}: {e}")
return result
return None
def get_range(self, key, gtype):
print(f"[DEBUG] Получение диапазона: key={key}, gtype={gtype}")
result = self._run_binary('get_range', key)
if not result:
print(f"[ERROR] Пустой результат или ошибка при выполнении команды get_range для ключа {key}")
return None
try:
parsed_result = ast.literal_eval(result)
return parsed_result
except (ValueError, SyntaxError) as e:
print(f"[ERROR] Ошибка при преобразовании результата {result} для ключа {key}: {e}")
return None
def set_value(self, key, value, gtype):
print(f"[DEBUG] Установка значения: key={key}, value={value}, gtype={gtype}")
result = self._run_binary('set_value', key, str(value))
if result:
try:
return ast.literal_eval(result)
except (ValueError, SyntaxError) as e:
print(f"[ERROR] Ошибка при преобразовании результата {result}: {e}")
return None
import os
import re
from .base import Backend
class FileBackend(Backend):
def __init__(self, params=None):
super().__init__(params)
self.file_path = os.path.expanduser(params.get('file_path'))
self.file_path = os.path.expandvars(self.file_path)
self.lines = []
self.vars = {}
self._parse_file()
def _parse_file(self):
if os.path.exists(self.file_path):
with open(self.file_path, 'r') as f:
self.lines = f.readlines()
for line_num, line in enumerate(self.lines):
self._parse_line(line_num, line)
def _parse_line(self, line_num, line):
line = line.rstrip('\n')
parsed = {
'raw': line,
'active': True,
'var_name': None,
'value': None,
'comment': '',
'style': {}
}
if re.match(r'^\s*#', line):
parsed['active'] = False
line = re.sub(r'^\s*#', '', line, count=1)
var_match = re.match(
r'^\s*([A-Za-z_][A-Za-z0-9_]*)\s*([=])\s*(.*?)(\s*(#.*)?)$',
line
)
if var_match:
parsed['var_name'] = var_match.group(1)
parsed['value'] = self._parse_value(var_match.group(3))
parsed['comment'] = var_match.group(4) or ''
value_part = var_match.group(3)
parsed['style'] = {
'space_before': ' ' if ' ' in var_match.group(0).split('=')[0][-1:] else '',
'space_after': ' ' if ' ' in var_match.group(0).split('=')[1][:1] else '',
'quote': self._detect_quote(var_match.group(3)),
'commented': not parsed['active']
}
if parsed['var_name'] not in self.vars:
self.vars[parsed['var_name']] = []
self.vars[parsed['var_name']].append((line_num, parsed))
@staticmethod
def _parse_value(value_str):
value_str = value_str.strip()
for quote in ['"', "'"]:
if value_str.startswith(quote) and value_str.endswith(quote):
return value_str[1:-1]
return value_str
@staticmethod
def _detect_quote(value_str):
value_str = value_str.strip()
if value_str.startswith('"') and value_str.endswith('"'):
return '"'
if value_str.startswith("'") and value_str.endswith("'"):
return "'"
return ''
def _get_style_template(self):
if not self.vars:
return {
'space_before': '',
'space_after': ' ',
'quote': '"',
'commented': False
}
last_var = next(reversed(self.vars.values()))[-1][1]
return last_var['style']
@staticmethod
def _build_line(key, value, style):
quote = style['quote']
value_str = f"{quote}{value}{quote}" if quote else str(value)
return (
f"{key}{style['space_before']}="
f"{style['space_after']}{value_str}"
)
def _save_file(self):
with open(self.file_path, 'w') as f:
f.writelines(self.lines)
def get_value(self, key, gtype):
entries = self.vars.get(key, [])
for entry in reversed(entries):
return entry[1]['value']
return None
def set_value(self, key, value, gtype):
entries = self.vars.get(key, [])
style = self._get_style_template()
key = os.path.expanduser(key)
key = os.path.expandvars(key)
if entries:
line_num, last_entry = entries[-1]
style = last_entry['style']
line = self._build_line(key, value, style)
self.lines[line_num] = line + '\n'
if last_entry['style']['commented']:
self.lines[line_num] = self.lines[line_num].lstrip('#')
else:
line = self._build_line(key, value, style)
self.lines.append(line + '\n')
self._save_file()
from gi.repository import Gio, GLib
from .base import Backend
class GSettingsBackend(Backend):
def get_value(self, key, gtype):
schema_name, key_name = key.rsplit('.', 1)
schema = Gio.Settings.new(schema_name)
print(f"[DEBUG] Получение значения: schema={schema_name}, key={key_name}, gtype={gtype}")
try:
value = schema.get_value(key_name)
return value.unpack()
except Exception as e:
print(f"[ERROR] Ошибка при получении значения {key}: {e}")
return None
def get_range(self, key, gtype):
schema_name, key_name = key.rsplit('.', 1)
schema = Gio.Settings.new(schema_name)
print(f"[DEBUG] Получение значения: schema={schema_name}, key={key_name}, gtype={gtype}")
try:
value = schema.get_range(key_name)
return value.unpack()[1]
except Exception as e:
print(f"[ERROR] Ошибка при получении значения {key}: {e}")
return None
def set_value(self, schema_key, value, gtype):
schema_name, key_name = schema_key.rsplit('.', 1)
schema = Gio.Settings.new(schema_name)
print(f"[DEBUG] Установка значения: schema={schema_name}, key={key_name}, value={value}, gtype={gtype}")
try:
schema.set_value(key_name, GLib.Variant(gtype, value))
except Exception as e:
print(f"[ERROR] Ошибка при установке значения {schema_key}: {e}")
import dbus
import ast
class DaemonClient:
def __new__(cls, bus_name="ru.ximperlinux.TuneIt.Daemon", object_path="/Daemon"):
"""
Создает экземпляр клиента только в случае, если сервис доступен.
:param bus_name: Имя D-Bus сервиса.
:param object_path: Путь объекта в D-Bus.
:return: Экземпляр DaemonClient или None, если сервис недоступен.
"""
try:
bus = dbus.SystemBus()
bus.get_object(bus_name, object_path) # Проверка доступности объекта
return super(DaemonClient, cls).__new__(cls)
except dbus.DBusException:
print(f"Service '{bus_name}' is not running.")
return None
def __init__(self, bus_name="ru.ximperlinux.TuneIt.Daemon", object_path="/Daemon"):
"""
Инициализация клиента для взаимодействия с D-Bus сервисом.
:param bus_name: Имя D-Bus сервиса.
:param object_path: Путь объекта в D-Bus.
"""
self.bus_name = bus_name
self.object_path = object_path
self.bus = dbus.SystemBus()
self.proxy = self.bus.get_object(bus_name, object_path)
self.interface = dbus.Interface(
self.proxy, dbus_interface="ru.ximperlinux.TuneIt.DaemonInterface"
)
print("dbus client connected")
self.backend_name = None
self.backend_params = None
def set_backend_name(self, backend_name):
"""
Устанавливает имя backend.
:param backend_name: Имя backend.
"""
self.backend_name = backend_name
def set_backend_params(self, backend_params):
"""
Устанавливает параметры backend.
:param backend_params: Параметры backend в формате JSON.
"""
self.backend_params = str(backend_params)
def get_value(self, key, gtype):
"""
Вызывает метод GetValue на D-Bus сервисе.
:param key: Ключ для получения значения.
:param gtype: Тип значения.
:return: Полученное значение.
"""
try:
return ast.literal_eval(str(self.interface.GetValue(self.backend_name, str(self.backend_params), key, gtype)))
except dbus.DBusException as e:
print(f"Error in GetValue: {e}")
return None
def set_value(self, key, value, gtype):
"""
Вызывает метод SetValue на D-Bus сервисе.
:param key: Ключ для установки значения.
:param value: Устанавливаемое значение.
:param gtype: Тип значения.
:return: Результат операции.
"""
try:
self.interface.SetValue(self.backend_name, str(self.backend_params), key, str(value), gtype)
except dbus.DBusException as e:
print(f"Error in SetValue: {e}")
def get_range(self, key, gtype):
"""
Вызывает метод GetRange на D-Bus сервисе.
:param key: Ключ для получения диапазона.
:param gtype: Тип значения.
:return: Диапазон значений.
"""
try:
return ast.literal_eval(str(self.interface.GetRange(self.backend_name, str(self.backend_params), key, gtype)))
except dbus.DBusException as e:
print(f"Error in GetRange: {e}")
return None
dclient = DaemonClient()
from .module import Module
from .page import Page
from .sections import SectionFactory
from .tools.yml_tools import load_modules
def init_settings_stack(stack, listbox, split_view):
yaml_data = load_modules()
section_factory = SectionFactory()
modules_dict = {}
pages_dict = {}
if stack.get_pages():
print("Clear pages...")
listbox.remove_all()
for page in stack.get_pages():
stack.remove(page)
else:
print("First init...")
for module_data in yaml_data:
module = Module(module_data)
modules_dict[module.name] = module
for section_data in module_data.get('sections', []):
page_name = module.get_translation(section_data.get('page', 'Default'))
module_page_name = section_data.get('page', 'Default')
print(module_page_name)
if page_name not in pages_dict:
page_info = (
module.pages.get(f"_{module_page_name}", {})
or module.pages.get(module_page_name, {})
)
page = Page(
name=page_name,
icon=page_info.get('icon'),
)
pages_dict[page_name] = page
section = section_factory.create_section(section_data, module)
pages_dict[page_name].add_section(section)
pages = list(pages_dict.values())
for page in pages:
page.sort_sections()
pages = sorted(pages, key=lambda p: p.name)
for page in pages:
page.create_stack_page(stack, listbox)
if not stack:
print("Ошибка: settings_pagestack не найден.")
def on_row_selected(listbox, row):
if row:
page_id = row.props.name
print(f"Selected page: {page_id}")
visible_child = stack.get_child_by_name(page_id)
if visible_child:
stack.set_visible_child(visible_child)
split_view.set_show_content(True)
listbox.connect("row-selected", on_row_selected)
import gettext
import locale
import os
class Module:
def __init__(self, module_data):
self.name = module_data['name']
self.weight = module_data.get('weight', 0)
self.path = module_data.get("module_path")
print(self.path)
self.pages = {
page['name']: page for page in module_data.get('pages', [])
}
self.sections = []
self.system_lang_code = self.get_system_language()
def add_section(self, section):
self.sections.append(section)
def get_sorted_sections(self):
return sorted(self.sections, key=lambda s: s.weight)
@staticmethod
def get_system_language():
lang, _ = locale.getdefaultlocale()
return lang.split('_')[0] if lang else 'en'
def get_translation(self, text, lang_code=None):
if text.startswith('_'):
text = text[1:]
locales_path = os.path.join(self.path, "locale")
if os.path.exists(locales_path):
text = gettext.translation(
domain='messages',
localedir=locales_path,
fallback=True
).gettext(text)
return text
from gi.repository import Adw
from .widgets.panel_row import TuneItPanelRow
class Page:
def __init__(self, name, icon=None):
self.name = name
self.icon = icon or "preferences-system" # Значение по умолчанию
self.sections = []
def add_section(self, section):
self.sections.append(section)
def sort_sections(self):
self.sections = sorted(self.sections, key=lambda s: s.weight)
def create_stack_page(self, stack, listbox):
pref_page = Adw.PreferencesPage()
not_empty = False
for section in self.sections:
preferences_group = section.create_preferences_group()
if preferences_group:
pref_page.add(preferences_group)
not_empty = True
else:
print(f"Секция {section.name} не создала виджетов.")
if not_empty:
stack_page = stack.add_child(pref_page)
stack_page.set_title(self.name)
stack_page.set_name(self.name)
row = TuneItPanelRow()
row.props.name = self.name
row.props.title = self.name
row.props.icon_name = self.icon
listbox.append(row)
else:
print(f"the page {self.name} is empty, ignored")
import os
from .backends import FileBackend
class Searcher:
def __init__(self, search_paths, exclude_paths, exclude_names, recursive):
self.search_paths = [
os.path.expanduser(os.path.expandvars(path))
for path in search_paths
]
self.exclude_paths = [
os.path.expanduser(os.path.expandvars(path))
for path in exclude_paths
]
self.exclude_names = exclude_names
self.recursive = recursive
def is_excluded(self, path):
abs_path = os.path.abspath(path)
for excluded in self.exclude_paths:
abs_excluded = os.path.abspath(excluded)
if abs_path == abs_excluded or abs_path.startswith(abs_excluded + os.sep):
return True
path_parts = os.path.normpath(abs_path).split(os.sep)
if any(part in self.exclude_names for part in path_parts):
return True
return False
class DirSearcher(Searcher):
def search(self):
result = []
for base_path in self.search_paths:
if not os.path.isdir(base_path) or self.is_excluded(base_path):
continue
if self.recursive:
for root, dirs, _ in os.walk(base_path):
dirs[:] = [d for d in dirs if not self.is_excluded(os.path.join(root, d))]
if self.is_excluded(root):
continue
result.append(root)
else:
try:
result.extend([
os.path.join(base_path, d)
for d in os.listdir(base_path)
if os.path.isdir(os.path.join(base_path, d))
and not self.is_excluded(os.path.join(base_path, d))
])
except PermissionError:
pass
return result
class FileSearcher(Searcher):
def __init__(self, search_paths, exclude_paths, exclude_names, recursive, pattern):
super().__init__(search_paths, exclude_paths, exclude_names, recursive)
self.pattern = pattern
def search(self):
result = []
for base_path in self.search_paths:
if not os.path.exists(base_path):
continue
if self.recursive and os.path.isdir(base_path):
for root, dirs, files in os.walk(base_path):
dirs[:] = [d for d in dirs if not self.is_excluded(os.path.join(root, d))]
if self.is_excluded(root):
continue
result.extend([
os.path.join(root, f)
for f in files
if f.endswith(self.pattern)
and not self.is_excluded(os.path.join(root, f))
])
else:
if os.path.isfile(base_path) and base_path.endswith(self.pattern):
if not self.is_excluded(base_path):
result.append(base_path)
else:
try:
result.extend([
os.path.join(base_path, f)
for f in os.listdir(base_path)
if os.path.isfile(os.path.join(base_path, f))
and f.endswith(self.pattern)
and not self.is_excluded(os.path.join(base_path, f))
])
except PermissionError:
pass
return result
class ValueInFileSearcher(Searcher):
def __init__(self, search_paths, exclude_paths, exclude_names, recursive,
file_pattern, key, exclude_neighbor_files=None):
super().__init__(search_paths, exclude_paths, exclude_names, recursive)
self.file_pattern = file_pattern
self.key = key
self.exclude_neighbor_files = exclude_neighbor_files or []
def has_exclude_neighbor(self, file_path):
dir_path = os.path.dirname(file_path)
return any(
os.path.exists(os.path.join(dir_path, neighbor))
for neighbor in self.exclude_neighbor_files
)
def search(self):
result = []
file_searcher = FileSearcher(
self.search_paths,
self.exclude_paths,
self.exclude_names,
self.recursive,
self.file_pattern
)
for file_path in file_searcher.search():
try:
if self.exclude_neighbor_files and self.has_exclude_neighbor(file_path):
continue
result.append(FileBackend(params={'file_path': file_path}).get_value(self.key, 's'))
except Exception as e:
print(f"Error processing {file_path}: {str(e)}")
continue
return result
class SearcherFactory:
_searchers = {
'dir': DirSearcher,
'file': FileSearcher,
'value_in_file': ValueInFileSearcher
}
@classmethod
def create(cls, config):
searcher_type = config['type']
params = {
'search_paths': config['search_paths'],
'exclude_paths': config.get('exclude_paths', []),
'exclude_names': config.get('exclude_names', []),
'recursive': config.get('recursive', True)
}
if searcher_type == 'file':
params['pattern'] = config['pattern']
elif searcher_type == 'value_in_file':
params['file_pattern'] = config['file_pattern']
params['key'] = config['key']
params['exclude_neighbor_files'] = config.get('exclude_neighbor_files', [])
return cls._searchers[searcher_type](**params)
from ..setting.setting import Setting
from .base_strategy import SectionStrategy
from .classic import ClassicSectionStrategy
class Section:
def __init__(self, section_data, strategy, module):
self.name = module.get_translation(section_data['name'])
self.weight = section_data.get('weight', 0)
self.page = section_data.get('page')
self.settings = [Setting(s, module) for s in section_data.get('settings', [])]
self.strategy = strategy
self.module = module
self.module.add_section(self)
def create_preferences_group(self):
return self.strategy.create_preferences_group(self)
class SectionFactory:
def __init__(self):
self.strategies = {
'classic': ClassicSectionStrategy(),
}
def create_section(self, section_data, module):
section_type = section_data.get('type', 'classic')
strategy = self.strategies.get(section_type)
if not strategy:
raise ValueError(f"Неизвестный тип секции: {section_type}")
return Section(section_data, strategy, module)
class SectionStrategy:
def create_preferences_group(self, section):
raise NotImplementedError("Метод create_preferences_group должен быть реализован")
from gi.repository import Adw
from .base_strategy import SectionStrategy
class ClassicSectionStrategy(SectionStrategy):
def create_preferences_group(self, section):
group = Adw.PreferencesGroup(title=section.name, description=section.module.name)
not_empty = False
for setting in section.settings:
row = setting.create_row()
if row:
print(f"Добавление строки для настройки: {setting.name}")
group.add(row)
not_empty = True
else:
print(f"Не удалось создать строку для настройки: {setting.name}")
if not_empty:
return group
else:
return None
from ..searcher import SearcherFactory
from .widgets import WidgetFactory
from ..backends import backend_factory
from ..daemon_client import dclient
from ..tools.gvariant import convert_by_gvariant
from ..widgets.service_dialog import ServiceNotStartedDialog
dialog_presented = False
class Setting:
def __init__(self, setting_data, module):
self._ = module.get_translation
self.name = self._(setting_data['name'])
self.root = setting_data.get('root', False)
self.backend = setting_data.get('backend')
self.params = {
**setting_data.get('params', {}),
'module_path': module.path
}
self.type = setting_data['type']
self.help = setting_data.get('help', None)
if self.help is not None:
self.help = self._(self.help)
self.key = setting_data.get('key')
self.default = setting_data.get('default')
self.gtype = setting_data.get('gtype', [])
self.search_target = setting_data.get('search_target', None)
self.map = setting_data.get('map')
if self.map is None:
if self.search_target is not None:
self.map = SearcherFactory.create(self.search_target).search()
else:
self.map = self._default_map()
if isinstance(self.map, list) and 'choice' in self.type:
self.map = {
item.title(): item for item in self.map
}
if isinstance(self.map, dict) and 'choice' in self.type:
self.map = {
self._(key) if isinstance(key, str) else key: value
for key, value in self.map.items()
}
if len(self.gtype) > 2:
self.gtype = self.gtype[0]
else:
self.gtype = self.gtype
def _default_map(self):
if self.type == 'boolean':
# Дефолтная карта для булевых настроек
return {True: True, False: False}
if 'choice' in self.type:
# Дефолтная карта для выборов
map = {}
range = self._get_backend_range()
if range is None:
return {}
for var in range:
print(var)
map[var[0].upper() + var[1:]] = var
return map
if self.type == 'number':
map = {}
range = self._get_backend_range()
if range is None:
return {}
map["upper"] = range[1]
map["lower"] = range[0]
# Кол-во после запятой
map["digits"] = len(str(range[0]).split('.')[-1]) if '.' in str(range[0]) else 0
# Минимальное число с этим количеством
map["step"] = 10 ** -map["digits"] if map["digits"] > 0 else 0
return map
return {}
def create_row(self):
if self.root is True:
print("Root is true")
if dclient is not None:
widget = WidgetFactory.create_widget(self)
return widget.create_row() if widget else None
else:
global dialog_presented
if dialog_presented is False:
from ..main import get_main_window
dialog = ServiceNotStartedDialog()
dialog.present(get_main_window())
dialog_presented = True
return None
widget = WidgetFactory.create_widget(self)
return widget.create_row() if widget else None
def _get_selected_row_index(self):
current_value = self._get_backend_value()
return list(self.map.values()).index(current_value) if current_value in self.map.values() else 0
def _get_default_row_index(self):
return list(self.map.values()).index(self.default) if self.default in self.map.values() else None
def _get_backend_value(self):
value = None
backend = self._get_backend()
if backend:
value = backend.get_value(self.key, self.gtype)
if value is None:
value = self.default
return value
def _get_backend_range(self):
backend = self._get_backend()
if backend:
return backend.get_range(self.key, self.gtype)
def _set_backend_value(self, value):
backend = self._get_backend()
if backend:
backend.set_value(self.key, convert_by_gvariant(value, self.gtype), self.gtype)
def _get_backend(self):
if self.root is True:
backend = dclient
backend.set_backend_name(self.backend)
backend.set_backend_params(self.params)
else:
backend = backend_factory.get_backend(self.backend, self.params)
if not backend:
print(f"Бекенд {self.backend} не зарегистрирован.")
return backend
from gi.repository import Gtk
class BaseWidget:
def __init__(self, setting):
self.setting = setting
self.reset_button = Gtk.Button(
icon_name="edit-undo-symbolic",
valign=Gtk.Align.CENTER,
halign=Gtk.Align.CENTER,
tooltip_text=_("Restore Default")
)
self.reset_button.add_css_class('flat')
self.reset_button.connect("clicked", self._on_reset_clicked)
self.reset_revealer = Gtk.Revealer(
transition_type=Gtk.RevealerTransitionType.CROSSFADE,
transition_duration=150,
child=self.reset_button,
reveal_child=False,
halign=Gtk.Align.END
)
def create_row(self):
raise NotImplementedError("Метод create_row должен быть реализован в подклассе")
def _on_reset_clicked(self, button):
raise NotImplementedError("Метод _on_reset_clicked должен быть реализован в подклассе")
from gi.repository import Adw, Gtk
from .BaseWidget import BaseWidget
class BooleanWidget(BaseWidget):
def create_row(self):
self.row = Adw.ActionRow(title=self.setting.name, subtitle=self.setting.help)
self.switch = Gtk.Switch(
valign=Gtk.Align.CENTER,
halign=Gtk.Align.CENTER,
)
self.switch.connect("notify::active", self._on_boolean_toggled)
control_box = Gtk.Box(spacing=6, orientation=Gtk.Orientation.HORIZONTAL)
control_box.append(self.reset_revealer)
control_box.append(self.switch)
self.row.add_suffix(control_box)
self._update_initial_state()
return self.row
def _update_initial_state(self):
current_value = self.setting._get_backend_value()
is_active = current_value == self.setting.map.get(True)
self.switch.set_active(is_active)
self._update_reset_visibility()
def _on_boolean_toggled(self, switch, _):
value = self.setting.map.get(True) if switch.get_active() else self.setting.map.get(False)
self.setting._set_backend_value(value)
self._update_reset_visibility()
def _on_reset_clicked(self, button):
default_value = self.setting.map.get(self.setting.default)
self.setting._set_backend_value(default_value)
self.switch.set_active(self.setting.default)
self._update_reset_visibility()
def _update_reset_visibility(self):
current_value = self.setting._get_backend_value()
default_value = self.setting.map.get(self.setting.default)
self.reset_revealer.set_reveal_child(
current_value != default_value if default_value is not None
else False
)
from gi.repository import Adw, Gtk
from .BaseWidget import BaseWidget
class ChoiceWidget(BaseWidget):
def create_row(self):
items = list(self.setting.map.keys())
self.row = Adw.ActionRow(title=self.setting.name, subtitle=self.setting.help)
self.dropdown = Gtk.DropDown.new_from_strings(items)
self.dropdown.set_halign(Gtk.Align.CENTER)
self.dropdown.set_valign(Gtk.Align.CENTER)
self._set_dropdown_width(items)
self.dropdown.set_selected(self.setting._get_selected_row_index())
self.dropdown.connect("notify::selected", self._on_choice_changed)
control_box = Gtk.Box(spacing=6, orientation=Gtk.Orientation.HORIZONTAL)
control_box.append(self.reset_revealer)
control_box.append(self.dropdown)
self.row.add_suffix(control_box)
self._update_reset_visibility()
return self.row
def _set_dropdown_width(self, items):
layout = self.dropdown.create_pango_layout("")
width = 0
for item in items:
layout.set_text(item)
text_width = layout.get_pixel_size()[0]
if text_width > width:
width = text_width
self.dropdown.set_size_request(width + 50, -1)
def _on_choice_changed(self, dropdown, _):
selected = dropdown.get_selected()
selected_value = list(self.setting.map.values())[selected]
self.setting._set_backend_value(selected_value)
self._update_reset_visibility()
def _on_reset_clicked(self, button):
default_value = self.setting._get_default_row_index()
if default_value is not None:
self.dropdown.set_selected(default_value)
self.setting._set_backend_value(self.setting.default)
self._update_reset_visibility()
def _update_reset_visibility(self):
current_value = self.setting._get_selected_row_index()
default_value = self.setting._get_default_row_index()
self.reset_revealer.set_reveal_child(
current_value != default_value
if default_value is not None
else False
)
\ No newline at end of file
from gi.repository import Gtk, Adw
from .BaseWidget import BaseWidget
class EntryWidget(BaseWidget):
def create_row(self):
self.row = Adw.ActionRow(title=self.setting.name)
self.entry = Gtk.Entry()
self.entry.set_halign(Gtk.Align.CENTER)
self.entry.set_text(self.setting._get_backend_value() or "")
self.entry.connect("activate", self._on_text_changed)
control_box = Gtk.Box(
orientation=Gtk.Orientation.HORIZONTAL,
spacing=6,
margin_start=12,
valign=Gtk.Align.CENTER,
halign=Gtk.Align.CENTER,
)
control_box.append(self.reset_revealer)
control_box.append(self.entry)
self.row.add_suffix(control_box)
self._update_reset_visibility()
return self.row
def _on_text_changed(self, entry):
new_value = entry.get_text()
self.setting._set_backend_value(new_value)
self._update_reset_visibility()
def _on_reset_clicked(self, button):
default_value = self.setting.default
self.setting._set_backend_value(default_value)
self.entry.set_text(str(default_value))
self._update_reset_visibility()
def _update_reset_visibility(self):
current_value = self.entry.get_text() or ""
default_value = self.setting.default
has_default = self.setting.default is not None
is_default = current_value == default_value
self.reset_revealer.set_reveal_child(not is_default and has_default)
\ No newline at end of file
from gi.repository import Adw, Gtk
from gi.repository import Gio
import os
from .BaseWidget import BaseWidget
class FileChooser(BaseWidget):
def create_row(self):
self.value_separated = False
self.multiple_mode = self.setting.map.get('multiple', False)
self.folder_mode = 'folder' in self.setting.map.get('extensions', [])
row = Adw.ActionRow(
title=self.setting.name,
subtitle=self.setting.help,
subtitle_selectable=True
)
control_box = Gtk.Box(
spacing=6,
margin_end=12,
halign=Gtk.Align.END
)
control_box.append(self.reset_revealer)
if not self.multiple_mode and not self.folder_mode:
self.entry = Gtk.Entry(
placeholder_text="Enter path or click to browse",
hexpand=True,
valign=Gtk.Align.CENTER,
halign=Gtk.Align.END,
)
self.entry.connect("changed", self._on_entry_changed)
control_box.append(self.entry)
else:
self.info_label = Gtk.Label(
label="No selection" if self.folder_mode else "No files selected",
valign=Gtk.Align.CENTER,
css_classes=["dim-label"]
)
control_box.append(self.info_label)
self.select_button = Gtk.Button.new_from_icon_name(
icon_name="folder-open-symbolic"
)
self.select_button.set_valign(Gtk.Align.CENTER)
self.select_button.connect("clicked", self._on_button_clicked)
control_box.append(self.select_button)
row.add_suffix(control_box)
self._update_display()
self._update_reset_visibility()
return row
def _on_reset_clicked(self, button):
default_value = self.setting.default
if default_value is not None:
if isinstance(default_value, str) and default_value.startswith("file://"):
default_value = default_value[7:]
self.setting._set_backend_value(default_value)
self._update_display()
self._update_reset_visibility()
def _update_reset_visibility(self):
current_value = self.setting._get_backend_value()
default_value = self.setting.default
if isinstance(current_value, str) and current_value.startswith("file://"):
current_value = current_value[7:]
if current_value:
current_value = os.path.expanduser(current_value)
current_value = os.path.expandvars(current_value)
if default_value:
default_value = os.path.expanduser(default_value)
default_value = os.path.expandvars(default_value)
self.reset_revealer.set_reveal_child(
current_value != default_value if default_value is not None
else False
)
def _update_display(self):
current = self.setting._get_backend_value()
self._update_reset_visibility()
if current and isinstance(current, str) and current.startswith("file://"):
current = current[7:]
self.value_separated = True
if self.folder_mode:
self._update_folder_display(current)
elif self.multiple_mode:
self._update_multiple_files_display(current)
else:
self._update_single_file_display(current)
def _on_button_clicked(self, button):
dialog = Gtk.FileDialog()
# Настройка фильтров
if not self.folder_mode and 'extensions' in self.setting.map:
filters = self._create_file_filters()
if filters.get_n_items() > 0:
dialog.set_filters(filters)
dialog.set_default_filter(filters.get_item(0))
# Установка начальной директории
current = self.setting._get_backend_value()
if current:
try:
current = os.path.expanduser(current)
current = os.path.expandvars(current)
current_file = Gio.File.new_for_path(current)
parent = current_file.get_parent() if not self.folder_mode else current_file
dialog.set_initial_folder(parent)
except Exception as e:
print(f"Error setting initial folder: {e}")
# Выбор метода открытия
try:
if self.folder_mode:
dialog.select_folder(
parent=button.get_root(),
callback=self._on_folder_selected
)
elif self.multiple_mode:
dialog.open_multiple(
parent=button.get_root(),
callback=self._on_files_selected
)
else:
dialog.open(
parent=button.get_root(),
callback=self._on_file_selected
)
except Exception as e:
print(f"File dialog error: {e}")
def _create_file_filters(self):
filters = Gio.ListStore.new(Gtk.FileFilter)
patterns = [p for p in self.setting.map.get('extensions', []) if p != 'folder']
if patterns:
file_filter = Gtk.FileFilter()
file_filter.set_name("Supported files")
for pattern in patterns:
if pattern.startswith('.'):
file_filter.add_suffix(pattern[1:])
else:
file_filter.add_pattern(pattern)
filters.append(file_filter)
return filters
def _on_file_selected(self, dialog, result):
try:
file = dialog.open_finish(result)
if file:
self.setting._set_backend_value(file.get_path())
self._update_display()
except Exception as e:
print(f"File selection error: {e}")
def _on_files_selected(self, dialog, result):
try:
file_list = dialog.open_multiple_finish(result)
if file_list:
paths = [f.get_path() for f in file_list]
self.setting._set_backend_value(paths)
self._update_display()
except Exception as e:
print(f"Multiple files selection error: {e}")
def _on_folder_selected(self, dialog, result):
try:
folder = dialog.select_folder_finish(result)
if folder:
self.setting._set_backend_value(folder.get_path())
self._update_display()
except Exception as e:
print(f"Folder selection error: {e}")
def _update_folder_display(self, current):
if current:
folder = Gio.File.new_for_path(current)
self.info_label.set_label(folder.get_basename() or current)
else:
self.info_label.set_label("No folder selected")
def _update_multiple_files_display(self, current):
count = len(current) if current else 0
self.info_label.set_label(f"{count} files selected" if count else "No files selected")
def _update_single_file_display(self, current):
self.entry.set_text(current or "")
if current:
file = Gio.File.new_for_path(current)
self.entry.set_tooltip_text(file.get_parse_name())
else:
self.entry.set_tooltip_text(None)
def _on_entry_changed(self, entry):
if not self.folder_mode and not self.multiple_mode:
path = entry.get_text().strip()
self.setting._set_backend_value(path)
self._update_reset_visibility()
\ No newline at end of file
from gi.repository import Adw, Gtk
from .BaseWidget import BaseWidget
class NumStepper(BaseWidget):
def create_row(self):
map = self.setting.map
map_keys = list(map.keys())
row = Adw.ActionRow(
title=self.setting.name,
subtitle=self.setting.help,
activatable=False
)
self.spin = Gtk.SpinButton(
valign=Gtk.Align.CENTER,
halign=Gtk.Align.CENTER,
)
adjustment = Gtk.Adjustment(
value=self.setting._get_backend_value(),
lower=map["lower"],
upper=map["upper"],
step_increment=map["step"],
)
self.spin.set_adjustment(adjustment)
if "digits" in map_keys:
self.spin.set_digits(map["digits"])
control_box = Gtk.Box(
orientation=Gtk.Orientation.HORIZONTAL,
spacing=6,
margin_start=12
)
control_box.append(self.reset_revealer)
control_box.append(self.spin)
row.add_suffix(control_box)
self.spin.connect("value-changed", self._on_num_changed)
self._update_reset_visibility()
return row
def _on_num_changed(self, widget):
selected_value = widget.get_value()
self.setting._set_backend_value(selected_value)
self._update_reset_visibility()
def _on_reset_clicked(self, button):
default_value = self.setting.default
if default_value is not None:
self.setting._set_backend_value(default_value)
self.spin.set_value(float(default_value))
self._update_reset_visibility()
def _update_reset_visibility(self):
current_value = float(self.setting._get_backend_value())
default_value = self.setting.default
self.reset_revealer.set_reveal_child(
current_value != default_value if default_value is not None
else False
)
\ No newline at end of file
from gi.repository import Gtk, Adw
from .BaseWidget import BaseWidget
class RadioChoiceWidget(BaseWidget):
def create_row(self):
main_box = Adw.PreferencesRow()
content_box = Gtk.Box(
orientation=Gtk.Orientation.VERTICAL,
spacing=6,
margin_top=8,
margin_bottom=8,
margin_start=12,
margin_end=12
)
main_box.set_child(content_box)
title_horizontal_box = Gtk.Box(
orientation=Gtk.Orientation.HORIZONTAL,
hexpand=True,
)
content_box.append(title_horizontal_box)
title_box = Gtk.Box(
orientation=Gtk.Orientation.VERTICAL,
hexpand=True,
spacing=1,
)
title_horizontal_box.append(title_box)
title_label = Gtk.Label(
label=self.setting.name,
halign=Gtk.Align.START,
)
title_box.append(title_label)
if self.setting.help:
subtitle_label = Gtk.Label(
label=self.setting.help,
halign=Gtk.Align.START,
wrap=True,
margin_bottom=4
)
subtitle_label.add_css_class("caption")
subtitle_label.add_css_class("dim-label")
title_box.append(subtitle_label)
radio_container = Gtk.Box(
orientation=Gtk.Orientation.VERTICAL,
spacing=8
)
content_box.append(radio_container)
self.radio_buttons = {}
current_value = self.setting._get_backend_value()
group = None
# Создаем радио-кнопки
for label, value in self.setting.map.items():
radio = Gtk.CheckButton(
label=label,
halign=Gtk.Align.START,
active=(value == current_value)
)
radio.add_css_class('selection-mode')
if group:
radio.set_group(group)
else:
group = radio
radio.connect("toggled", self._on_toggle, value)
radio_container.append(radio)
self.radio_buttons[value] = radio
self.reset_revealer.set_halign(Gtk.Align.END)
title_horizontal_box.append(self.reset_revealer)
self._update_reset_visibility()
return main_box
def _on_toggle(self, button, value):
if button.get_active():
self.setting._set_backend_value(value)
self._update_reset_visibility()
def _on_reset_clicked(self, button):
default_value = self.setting.default
if default_value is not None:
self.setting._set_backend_value(default_value)
if default_value in self.radio_buttons:
self.radio_buttons[default_value].set_active(True)
self._update_reset_visibility()
def _update_reset_visibility(self):
current_value = self.setting._get_backend_value()
default_value = self.setting.default
self.reset_revealer.set_reveal_child(
current_value != default_value if default_value is not None
else False
)
\ No newline at end of file
from .BooleanWidget import BooleanWidget
from .ChoiceWidget import ChoiceWidget
from .RadioChoiceWidget import RadioChoiceWidget
from .EntryWidget import EntryWidget
from .NumStepper import NumStepper
from .FileChooser import FileChooser
class WidgetFactory:
widget_map = {
'file': FileChooser,
'choice': ChoiceWidget,
'choice_radio': RadioChoiceWidget,
'boolean': BooleanWidget,
'entry': EntryWidget,
'number': NumStepper,
......
def convert_by_gvariant(value, gtype):
"""
Приводит значение к нужному типу в зависимости от GVariant gtype.
:param value: Исходное значение
:param gtype: Тип GVariant ('b', 'y', 'n', 'q', 'i', 'u', 'x', 't', 'd', 's')
:return: Значение, приведенное к указанному типу
"""
try:
if gtype == 'b': # Boolean
return bool(value)
elif gtype == 'y': # Byte
return max(0, min(255, int(value))) # Ограничение диапазона
elif gtype == 'n': # Int16
return max(-32768, min(32767, int(value))) # Ограничение диапазона
elif gtype == 'q': # Uint16
return max(0, min(65535, int(value))) # Ограничение диапазона
elif gtype == 'i': # Int32
return max(-2147483648, min(2147483647, int(value)))
elif gtype == 'u': # Uint32
return max(0, min(4294967295, int(value)))
elif gtype == 'x': # Int64
return max(-9223372036854775808, min(9223372036854775807, int(value)))
elif gtype == 't': # Uint64
return max(0, min(18446744073709551615, int(value)))
elif gtype == 'd': # Double
return float(value)
elif gtype == 's': # String
return str(value)
else:
raise ValueError(f"Неизвестный GVariant тип: {gtype}")
except (ValueError, TypeError) as e:
print(f"Ошибка приведения типа: {e}")
return None
......@@ -2,48 +2,66 @@ import os
import yaml
def get_local_share_directory():
def get_local_module_directory():
home_directory = os.path.expanduser("~")
local_share_directory = os.path.join(home_directory, ".local", "share", "tuneit")
return local_share_directory
return os.path.join(home_directory, ".local", "share", "tuneit", "modules")
def get_module_directory():
return "/usr/share/tuneit/modules"
def load_modules():
modules = []
local_share_directory = get_local_share_directory()
modules_directory = os.path.join(local_share_directory, "modules")
if not os.path.exists(modules_directory):
print(f"Директория {modules_directory} не существует")
return modules
modules = load_yaml_files_from_directory(modules_directory)
local_modules_directory = get_local_module_directory()
global_modules_directory = get_module_directory()
all_modules = set(os.listdir(global_modules_directory))
for module_name in os.listdir(local_modules_directory):
module_path = os.path.join(local_modules_directory, module_name)
if os.path.isdir(module_path):
modules += load_yaml_files_from_directory(module_path)
all_modules.discard(module_name)
for module_name in all_modules:
module_path = os.path.join(global_modules_directory, module_name)
if os.path.isdir(module_path):
modules += load_yaml_files_from_directory(module_path)
return modules
def load_yaml_files_from_directory(directory):
yaml_data = []
for root, _, files in os.walk(directory):
for file in files:
for file in os.listdir(directory):
if file.endswith(".yml") or file.endswith(".yaml"):
file_path = os.path.join(directory, file)
with open(file_path, 'r', encoding='utf-8') as f:
try:
data = yaml.safe_load(f)
if data:
for item in data:
item['module_path'] = directory
yaml_data.extend(data)
except yaml.YAMLError as e:
print(f"Ошибка при чтении файла {file_path}: {e}")
sections_data = []
sections_directory = os.path.join(directory, 'sections')
if os.path.exists(sections_directory) and os.path.isdir(sections_directory):
for file in os.listdir(sections_directory):
if file.endswith(".yml") or file.endswith(".yaml"):
file_path = os.path.join(root, file)
file_path = os.path.join(sections_directory, file)
with open(file_path, 'r', encoding='utf-8') as f:
try:
data = yaml.safe_load(f)
if data:
yaml_data.extend(data)
sections_data.extend(data)
except yaml.YAMLError as e:
print(f"Ошибка при чтении файла {file_path}: {e}")
return yaml_data
def merge_categories_by_name(categories_data):
categories_dict = {}
for category_data in categories_data:
category_name = category_data['name']
if category_name not in categories_dict:
categories_dict[category_name] = category_data
for module in yaml_data:
if 'sections' in module:
module['sections'].extend(sections_data)
else:
categories_dict[category_name]['sections'].extend(
category_data['sections'])
return list(categories_dict.values())
module['sections'] = sections_data
return yaml_data
class BaseWidget:
def __init__(self, setting):
self.setting = setting
def create_row(self):
raise NotImplementedError("Метод create_row должен быть реализован в подклассе")
from gi.repository import Adw
from .BaseWidget import BaseWidget
class BooleanWidget(BaseWidget):
def create_row(self):
row = Adw.SwitchRow(title=self.setting.name, subtitle=self.setting.help)
current_value = self.setting._get_backend_value()
row.set_active(current_value == self.setting.map.get(True))
row.connect("notify::active", self._on_boolean_toggled)
return row
def _on_boolean_toggled(self, switch, _):
value = self.setting.map.get(True) if switch.get_active() else self.setting.map.get(False)
self.setting._set_backend_value(value)
from gi.repository import Adw, Gtk
from .BaseWidget import BaseWidget
class ChoiceWidget(BaseWidget):
def create_row(self):
row = Adw.ComboRow(title=self.setting.name, subtitle=self.setting.help)
row.set_model(Gtk.StringList.new(list(self.setting.map.keys())))
row.set_selected(self.setting._get_selected_row_index())
row.connect("notify::selected", self._on_choice_changed)
return row
def _on_choice_changed(self, combo_row, _):
selected_value = list(self.setting.map.values())[combo_row.get_selected()]
self.setting._set_backend_value(selected_value)
from gi.repository import Adw
from .BaseWidget import BaseWidget
class EntryWidget(BaseWidget):
def create_row(self):
row = Adw.EntryRow(title=self.setting.name)
row.set_show_apply_button(True)
row.set_text(self.setting._get_backend_value())
row.connect("apply", self._on_text_changed)
return row
def _on_text_changed(self, entry_row):
self.setting._set_backend_value(entry_row.get_text())
from docutils.nodes import subtitle
from gi.repository import Adw, Gtk
from .BaseWidget import BaseWidget
class NumStepper(BaseWidget):
def create_row(self):
map = self.setting.map
map_keys = list(self.setting.map.keys())
row = Adw.SpinRow(
title=self.setting.name, subtitle=self.setting.help
)
adjustment = Gtk.Adjustment(
value=self.setting._get_backend_value(),
lower=map["lower"], upper=map["upper"],
step_increment=map["step"],
)
row.set_adjustment(adjustment)
if "digits" in map_keys:
row.set_digits(map["digits"])
adjustment.connect("value_changed", self._on_num_changed)
return row
def _on_num_changed(self, adj):
selected_value = adj.get_value()
self.setting._set_backend_value(selected_value)
using Gtk 4.0;
template $TuneItPanelRow: ListBoxRow {
Box {
spacing: 12;
margin-top: 6;
margin-bottom: 6;
margin-start: 2;
margin-end: 2;
Image thumbnail_image {
icon-name: bind template.icon-name;
}
Box {
orientation: vertical;
valign: center;
spacing: 2;
Label title_label {
label: bind template.title;
halign: start;
justify: left;
wrap: true;
}
Label subtitle_label {
styles [
"caption",
"dim-label",
]
label: bind template.subtitle;
visible: bind template.subtitle-visible;
halign: start;
justify: left;
wrap: true;
}
}
}
}
from gi.repository import GObject, Adw, Gtk
@Gtk.Template(resource_path='/ru.ximperlinux.TuneIt/settings/widgets/panel_row.ui')
class TuneItPanelRow(Gtk.ListBoxRow):
__gtype_name__ = "TuneItPanelRow"
name = GObject.Property(type=str, default='')
title = GObject.Property(type=str, default='')
subtitle = GObject.Property(type=str, default='')
subtitle_visible = GObject.Property(type=bool, default=False)
icon_name = GObject.Property(type=str, default='')
import os
import subprocess
import sys
from gi.repository import Adw
class ServiceNotStartedDialog(Adw.AlertDialog):
def __init__(self):
super().__init__()
self.sname = 'tuneit-daemon'
self.set_heading(_("Dbus service is disabled or unresponsive."))
self.set_body(_("It is needed for modules that require root permissions.\nDo you want to try to turn on the service?\nTune It will restart after enabling the service."))
self.add_response("yes", _("Yes"))
self.add_response("no", _("No"))
self.connect("response", self.on_response)
def on_response(self, dialog, response):
if response == "yes":
self.service_enable()
dialog.close()
os.execv(sys.argv[0], sys.argv)
elif response in ("no", "close"):
dialog.close()
def service_status(self):
try:
# Запускаем команду systemctl is-active <service_name>
result = subprocess.run(
['systemctl', 'is-active', self.sname],
stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
# Проверяем статус
if result.stdout.decode('utf-8').strip() == 'active':
return True
else:
return False
except Exception as e:
print(f"An error occurred: {e}")
return False
def service_enable(self):
try:
subprocess.run(
['pkexec', 'systemctl', '--now', 'enable', self.sname],
stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
except Exception as e:
print(f"An error occurred: {e}")
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/ru/ximperlinux/TuteIt">
<gresource prefix="/ru.ximperlinux.TuneIt">
<file preprocess="xml-stripblanks">window.ui</file>
<file preprocess="xml-stripblanks">settings/widgets/panel_row.ui</file>
<file preprocess="xml-stripblanks">gtk/help-overlay.ui</file>
</gresource>
</gresources>
# import gettext
# gettext.textdomain('tuneit')
# _ = gettext.gettext
......@@ -2,22 +2,36 @@ using Gtk 4.0;
using Adw 1;
template $TuneitWindow: Adw.ApplicationWindow {
width-request: 360;
height-request: 294;
default-height: 600;
default-width: 800;
title: _("TuneIt");
Adw.Breakpoint {
condition ("max-width: 400sp")
condition ("max-width: 500sp")
setters {
header_bar.title-widget: null;
main_toolbar.top-bar-style: flat;
settinga_content_bar.visible: true;
switcher_bar.reveal: true;
header_view_switcher.visible: false;
main_toolbar.reveal-bottom-bars: true;
main_toolbar.reveal-top-bars: false;
settings_split_view.collapsed: true;
}
}
content: Adw.ToolbarView main_toolbar{
top-bar-style: raised_border;
content: Adw.ToolbarView main_toolbar {
top-bar-style: raised;
bottom-bar-style: raised;
reveal-bottom-bars: false;
[top]
Adw.HeaderBar header_bar {
[title]
Adw.ViewSwitcher header_view_switcher {
policy: wide;
stack: main_stack;
}
[end]
MenuButton {
icon-name: "open-menu-symbolic";
......@@ -25,51 +39,72 @@ template $TuneitWindow: Adw.ApplicationWindow {
primary: true;
tooltip-text: _("Main Menu");
}
[title]
Adw.ViewSwitcher {
policy: wide;
stack: main_stack;
}
}
Adw.ViewStack main_stack {
Adw.ViewStackPage {
child: Box {
Adw.NavigationSplitView settings_split_view {
hexpand: true;
content: Adw.NavigationPage {
Adw.ToolbarView {
[top]
Adw.HeaderBar settinga_content_bar {
decoration-layout: "";
visible: false;
icon-name: "preferences-system";
name: "settings";
title: _("Settings");
child: Adw.NavigationSplitView settings_split_view {
hexpand: true;
content: Adw.NavigationPage {
title: bind settings_pagestack.visible-child-name;
Adw.ToolbarView {
reveal-top-bars: bind main_toolbar.reveal-top-bars inverted;
[top]
Adw.HeaderBar header_bar2 {
[end]
MenuButton {
icon-name: "open-menu-symbolic";
menu-model: primary_menu;
primary: true;
tooltip-text: _("Main Menu");
}
}
Stack settings_pagestack {}
Stack settings_pagestack {}
}
};
sidebar: Adw.NavigationPage {
title: _("Sections");
Adw.ToolbarView {
reveal-top-bars: bind main_toolbar.reveal-top-bars inverted;
[top]
Adw.HeaderBar {
[end]
MenuButton {
icon-name: "open-menu-symbolic";
menu-model: primary_menu;
primary: true;
tooltip-text: _("Main Menu");
}
}
};
sidebar: Adw.NavigationPage {
Adw.ClampScrollable {
margin-bottom: 8;
margin-end: 8;
margin-start: 8;
margin-top: 8;
ListBox settings_listbox {
styles [
"navigation-sidebar",
]
ScrolledWindow {
propagate-natural-height: true;
hscrollbar-policy: never;
Adw.Clamp {
maximum-size: 500;
ListBox settings_listbox {
styles [
"navigation-sidebar",
]
}
}
}
};
}
}
};
};
icon-name: "preferences-system";
name: "settings";
title: _("Settings");
}
Adw.ViewStackPage {
......@@ -107,20 +142,16 @@ template $TuneitWindow: Adw.ApplicationWindow {
icon-name: "system-software-install-symbolic";
name: "shop";
title: _("Shop");
title: _("Shop");
}
}
[bottom]
Adw.ViewSwitcherBar switcher_bar {
reveal: true;
stack: main_stack;
}
};
default-height: 600;
default-width: 800;
title: _("TuneIt");
}
menu primary_menu {
......@@ -141,4 +172,3 @@ menu primary_menu {
}
}
}
......@@ -17,12 +17,13 @@
#
# SPDX-License-Identifier: GPL-3.0-or-later
from gi.repository import Adw, Gtk
import threading
from gi.repository import GObject, Adw, Gtk, GLib
from .settings import init_settings_stack
from .settings.main import init_settings_stack
from .shop import init_shop_stack
@Gtk.Template(resource_path='/ru/ximperlinux/TuteIt/window.ui')
@Gtk.Template(resource_path='/ru.ximperlinux.TuneIt/window.ui')
class TuneitWindow(Adw.ApplicationWindow):
__gtype_name__ = 'TuneitWindow'
......@@ -30,13 +31,37 @@ class TuneitWindow(Adw.ApplicationWindow):
settings_listbox = Gtk.Template.Child()
settings_split_view = Gtk.Template.Child()
shop_pagestack = Gtk.Template.Child()
shop_listbox = Gtk.Template.Child()
shop_split_view = Gtk.Template.Child()
@GObject.Signal
def settings_page_update(self):
pass
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.connect('settings_page_update', self.update_settings_page)
self.update_settings_page()
def update_settings_page(self):
thread = threading.Thread(target=self._update_settings_page)
thread.daemon = True
thread.start()
init_settings_stack(self.settings_pagestack, self.settings_listbox, self.settings_split_view)
init_shop_stack(self.shop_pagestack, self.shop_listbox, self.shop_split_view)
def _update_settings_page(self, *args):
"""
Можно вызвать вот так, благодаря сигналу:
self.settings_pagestack.get_root().emit("settings_page_update")
"""
init_settings_stack(
self.settings_pagestack,
self.settings_listbox,
self.settings_split_view,
)
init_shop_stack(
self.shop_pagestack,
self.shop_listbox,
self.shop_split_view,
)
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.94.1 -->
<interface>
<!-- interface-name window.ui -->
<requires lib="Adw" version="1.0"/>
<requires lib="gio" version="2.0"/>
<requires lib="gtk" version="4.0"/>
<requires lib="libadwaita" version="1.6"/>
<template class="TuneitWindow" parent="AdwApplicationWindow">
<property name="content">
<object class="AdwToolbarView">
<child type="top">
<object class="AdwHeaderBar">
<child type="end">
<object class="GtkMenuButton">
<property name="icon-name">open-menu-symbolic</property>
<property name="menu-model">primary_menu</property>
<property name="primary">True</property>
<property name="tooltip-text" translatable="yes">Main Menu</property>
</object>
</child>
<child type="title">
<object class="AdwViewSwitcher">
<property name="policy">wide</property>
<property name="stack">main_stack</property>
</object>
</child>
</object>
</child>
<child>
<object class="AdwViewStack" id="main_stack">
<child>
<object class="AdwViewStackPage">
<property name="child">
<object class="GtkBox">
<child>
<object class="AdwNavigationSplitView">
<property name="content">
<object class="AdwNavigationPage">
<child>
<object class="GtkStack" id="main_pagestack"/>
</child>
</object>
</property>
<property name="hexpand">True</property>
<property name="sidebar">
<object class="AdwNavigationPage">
<child>
<object class="AdwClampScrollable">
<property name="margin-bottom">8</property>
<property name="margin-end">8</property>
<property name="margin-start">8</property>
<property name="margin-top">8</property>
<child>
<object class="GtkStackSidebar">
<property name="stack">main_pagestack</property>
</object>
</child>
</object>
</child>
</object>
</property>
</object>
</child>
</object>
</property>
<property name="icon-name">preferences-system</property>
<property name="name">main</property>
<property name="title">main</property>
</object>
</child>
</object>
</child>
</object>
</property>
<property name="default-height">600</property>
<property name="default-width">800</property>
<property name="title" translatable="yes">TuneIt</property>
</template>
<menu id="primary_menu">
<section>
<item>
<attribute name="action">app.preferences</attribute>
<attribute name="label" translatable="yes">_Preferences</attribute>
</item>
<item>
<attribute name="action">win.show-help-overlay</attribute>
<attribute name="label" translatable="yes">_Keyboard Shortcuts</attribute>
</item>
<item>
<attribute name="action">app.about</attribute>
<attribute name="label" translatable="yes">_About TuneIt</attribute>
</item>
</section>
</menu>
</interface>
......@@ -38,19 +38,28 @@ where each setting is defined in separate files for flexibility and extensibilit
%install
%meson_install
%find_lang %name
%find_lang tuneit
%files -f %name.lang
%_bindir/%name
%files -f tuneit.lang
%_bindir/tuneit
%_datadir/%name
%_datadir/tuneit
%_datadir/glib-2.0/schemas/*.gschema.xml
%_datadir/metainfo/*.metainfo.xml
%_desktopdir/ru.ximperlinux.TuteIt.desktop
%_datadir/dbus-1/services/*ximper*.service
%_desktopdir/ru.ximperlinux.TuneIt.desktop
%_iconsdir/hicolor/*/apps/*.svg
%_sbindir/tuneit-daemon
%_unitdir/tuneit-daemon.service
%_datadir/polkit-1/actions/*ximper*.policy
%_sysconfdir/dbus-1/system.d/*ximper*.conf
%changelog
* Tue Dec 17 2024 Roman Alifanov <ximper@altlinux.org> 0.1.0-alt1
- initial build
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment