Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
T
tuneit
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Ximper Linux
tuneit
Commits
541c7426
Commit
541c7426
authored
Feb 03, 2025
by
Anton Palgunov
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
migrate code shop to main file
parent
9ffc0e1d
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
432 additions
and
432 deletions
+432
-432
__init__.py
src/shop/__init__.py
+0
-431
main.py
src/shop/main.py
+431
-0
window.py
src/window.py
+1
-1
No files found.
src/shop/__init__.py
View file @
541c7426
import
os
import
shutil
import
tempfile
import
threading
import
requests
import
time
import
yaml
import
gi
gi
.
require_version
(
"Gtk"
,
"4.0"
)
gi
.
require_version
(
"Adw"
,
"1"
)
from
gi.repository
import
Gtk
,
Adw
,
Gdk
,
GObject
,
GLib
## ToDo: Унести это в адаптеры для работы с API
GITHUB_API_URL
=
"https://api.github.com/search/repositories"
GITLAB_API_URL
=
"https://gitlab.com/api/v4/projects"
CACHE_DIR
=
os
.
path
.
join
(
os
.
path
.
expanduser
(
"~"
),
".cache"
,
"tuneit"
,
"shop"
)
if
not
os
.
path
.
exists
(
CACHE_DIR
):
os
.
makedirs
(
CACHE_DIR
,
exist_ok
=
True
)
MODULES_DIR
=
os
.
path
.
join
(
os
.
path
.
expanduser
(
"~"
),
".local"
,
"share"
,
"tuneit"
,
"modules"
)
if
not
os
.
path
.
exists
(
MODULES_DIR
):
os
.
makedirs
(
MODULES_DIR
,
exist_ok
=
True
)
###############################################################################
# Работа с репами и кеш
###############################################################################
def
load_from_cache
():
cache_file
=
os
.
path
.
join
(
CACHE_DIR
,
"modules_cache.json"
)
if
not
os
.
path
.
exists
(
cache_file
):
return
None
,
0
with
open
(
cache_file
,
"r"
,
encoding
=
"utf-8"
)
as
f
:
import
json
data
=
json
.
load
(
f
)
return
data
[
"repos"
],
data
[
"last_update"
]
def
save_to_cache
(
repos
):
cache_file
=
os
.
path
.
join
(
CACHE_DIR
,
"modules_cache.json"
)
with
open
(
cache_file
,
"w"
,
encoding
=
"utf-8"
)
as
f
:
import
json
json
.
dump
({
"last_update"
:
int
(
time
.
time
()),
"repos"
:
repos
},
f
,
ensure_ascii
=
False
,
indent
=
2
)
def
fetch_repos_from_github
(
search_query
,
page
=
1
,
per_page
=
30
):
# Пример https://api.github.com/search/repositories?q=tuneit-mod+in:name
params
=
{
"q"
:
search_query
+
" in:name"
,
"sort"
:
"stars"
,
"order"
:
"desc"
,
"page"
:
page
,
"per_page"
:
per_page
}
r
=
requests
.
get
(
GITHUB_API_URL
,
params
=
params
,
timeout
=
10
)
r
.
raise_for_status
()
data
=
r
.
json
()
return
data
.
get
(
"items"
,
[])
def
fetch_repos_from_gitlab
(
search_query
,
page
=
1
,
per_page
=
30
):
# Пример https://gitlab.com/api/v4/projects?search=tuneit-mod
params
=
{
"search"
:
search_query
,
"order_by"
:
"star_count"
,
"sort"
:
"desc"
,
"page"
:
page
,
"per_page"
:
per_page
}
r
=
requests
.
get
(
GITLAB_API_URL
,
params
=
params
,
timeout
=
10
)
r
.
raise_for_status
()
data
=
r
.
json
()
return
data
def
parse_module_yaml
(
raw_url
):
try
:
r
=
requests
.
get
(
raw_url
,
timeout
=
10
)
r
.
raise_for_status
()
return
yaml
.
safe_load
(
r
.
text
)
except
:
return
None
def
retrieve_modules_full_list
(
force_refresh
=
False
,
initial_page
=
1
,
per_page
=
30
):
"""
Реализует загрузку и логику суточного кеша
"""
# 1. Проверяем кеш
cached_repos
,
last_update
=
load_from_cache
()
day_in_seconds
=
24
*
60
*
60
now
=
int
(
time
.
time
())
# 2. Если кеш валиден и нет force_refresh
if
(
cached_repos
is
not
None
)
and
(
not
force_refresh
)
and
(
now
-
last_update
<
day_in_seconds
):
return
cached_repos
# 3. Иначе загружаем данные
aggregated_repos
=
[]
# GitHub
github_items
=
fetch_repos_from_github
(
"tuneit-mod"
,
page
=
initial_page
,
per_page
=
per_page
)
for
item
in
github_items
:
# Проверяем, что репозиторий (item["name"]) действительно начинается с tuneit-mod
if
not
item
[
"name"
]
.
startswith
(
"tuneit-mod"
):
continue
# Пробуем скачать module.yaml
print
(
item
)
raw_url
=
f
"https://raw.githubusercontent.com/{item['owner']['login']}/{item['name']}/{item['default_branch']}/module.yaml"
module_info
=
parse_module_yaml
(
raw_url
)
aggregated_repos
.
append
({
"source"
:
"github"
,
"repo_name"
:
item
[
"name"
],
"repo_full_name"
:
item
[
"full_name"
],
"stars"
:
item
[
"stargazers_count"
],
"branch"
:
item
[
"default_branch"
],
"description"
:
item
[
"description"
]
or
""
,
"html_url"
:
item
[
"html_url"
],
"star_url"
:
item
[
"html_url"
]
+
"/stargazers"
,
"module_yaml"
:
module_info
[
0
]
})
# GitLab
gitlab_items
=
fetch_repos_from_gitlab
(
"tuneit-mod"
,
page
=
initial_page
,
per_page
=
per_page
)
for
item
in
gitlab_items
:
if
not
item
[
"name"
]
.
startswith
(
"tuneit-mod"
):
continue
# Пробуем скачать module.yaml
namespace_and_repo
=
item
[
"path_with_namespace"
]
# "username/reponame"
raw_url
=
f
"https://gitlab.com/{namespace_and_repo}/-/raw/{item['default_branch']}/module.yaml"
module_info
=
parse_module_yaml
(
raw_url
)
aggregated_repos
.
append
({
"source"
:
"gitlab"
,
"repo_name"
:
item
[
"name"
],
"repo_full_name"
:
namespace_and_repo
,
"branch"
:
item
[
"default_branch"
],
"stars"
:
item
[
"star_count"
],
"description"
:
item
[
"description"
]
or
""
,
"html_url"
:
item
[
"web_url"
],
"star_url"
:
item
[
"web_url"
]
+
"/-/starrers"
,
"module_yaml"
:
module_info
[
0
]
})
# Сохраняем в кеш
save_to_cache
(
aggregated_repos
)
return
aggregated_repos
###############################################################################
# Логика UI
###############################################################################
def
download_and_install_repo
(
source
,
repo_full_name
,
repo_name
,
branch
):
"""
Скачивает ZIP-архив репозитория и распаковывает его
в папку ~/.local/share/tuneit/modules/<repo_name>.
"""
if
source
==
"github"
:
url
=
f
"https://github.com/{repo_full_name}/archive/refs/heads/{branch}.zip"
else
:
# GitLab:
url
=
f
"https://gitlab.com/{repo_full_name}/-/archive/{branch}/{repo_name}.zip"
# Временная папка
tmp_dir
=
tempfile
.
mkdtemp
()
zip_path
=
os
.
path
.
join
(
tmp_dir
,
f
"{repo_name}.zip"
)
try
:
r
=
requests
.
get
(
url
,
stream
=
True
)
r
.
raise_for_status
()
with
open
(
zip_path
,
"wb"
)
as
f
:
for
chunk
in
r
.
iter_content
(
chunk_size
=
8192
):
f
.
write
(
chunk
)
# Распакуем zip
import
zipfile
with
zipfile
.
ZipFile
(
zip_path
,
"r"
)
as
zip_ref
:
zip_ref
.
extractall
(
tmp_dir
)
# Найдём распакованную папку вида <repo_name>-main или что-то вроде
extracted_dirs
=
[
d
for
d
in
os
.
listdir
(
tmp_dir
)
if
os
.
path
.
isdir
(
os
.
path
.
join
(
tmp_dir
,
d
))
and
d
.
startswith
(
repo_name
)]
if
extracted_dirs
:
extracted_path
=
os
.
path
.
join
(
tmp_dir
,
extracted_dirs
[
0
])
# Создаём папку назначения
dest_path
=
os
.
path
.
join
(
MODULES_DIR
,
repo_name
)
# Если уже была папка, удалим её
if
os
.
path
.
exists
(
dest_path
):
shutil
.
rmtree
(
dest_path
)
shutil
.
move
(
extracted_path
,
dest_path
)
finally
:
shutil
.
rmtree
(
tmp_dir
,
ignore_errors
=
True
)
###############################################################################
# Виджет “карточка” модуля
###############################################################################
class
ModuleCard
(
Gtk
.
Box
):
"""
Отображает карточку модуля.
"""
__gtype_name__
=
"ModuleCard"
def
__init__
(
self
,
module_data
):
super
()
.
__init__
(
orientation
=
Gtk
.
Orientation
.
VERTICAL
,
spacing
=
6
)
self
.
module_data
=
module_data
# Настраиваем стили и отступы
self
.
set_margin_top
(
12
)
self
.
set_margin_bottom
(
12
)
self
.
set_margin_start
(
12
)
self
.
set_margin_end
(
12
)
# Фрейм или карточка
frame
=
Gtk
.
Frame
()
frame
.
set_margin_top
(
6
)
frame
.
set_margin_bottom
(
6
)
frame
.
set_margin_start
(
6
)
frame
.
set_margin_end
(
6
)
self
.
append
(
frame
)
card_box
=
Gtk
.
Box
(
orientation
=
Gtk
.
Orientation
.
VERTICAL
,
spacing
=
6
)
card_box
.
set_margin_top
(
6
)
card_box
.
set_margin_bottom
(
6
)
card_box
.
set_margin_start
(
6
)
card_box
.
set_margin_end
(
6
)
frame
.
set_child
(
card_box
)
# Обложка (cover)
cover_url
=
""
if
module_data
[
"module_yaml"
]:
cover_url
=
module_data
[
"module_yaml"
]
.
get
(
"cover"
,
""
)
if
cover_url
:
# TODO: Загрузка обложки в кеш и отображение
try
:
texture
=
Gdk
.
Texture
.
new_for_uri
(
cover_url
)
cover_image
=
Gtk
.
Image
.
new_from_paintable
(
texture
)
cover_image
.
set_size_request
(
200
,
150
)
# 800x600 – размер обложки
card_box
.
append
(
cover_image
)
except
:
pass
title_label
=
Gtk
.
Label
()
if
module_data
[
"module_yaml"
]
and
module_data
[
"module_yaml"
]
.
get
(
"name"
):
title_label
.
set_label
(
module_data
[
"module_yaml"
][
"name"
])
else
:
title_label
.
set_label
(
module_data
[
"repo_name"
])
title_label
.
set_xalign
(
0.0
)
title_label
.
set_markup
(
f
"<b>{title_label.get_label()}</b>"
)
card_box
.
append
(
title_label
)
# Описание
desc_label
=
Gtk
.
Label
()
desc_label
.
set_xalign
(
0.0
)
desc_label
.
set_wrap
(
True
)
if
module_data
[
"module_yaml"
]
and
module_data
[
"module_yaml"
]
.
get
(
"description"
):
desc_label
.
set_text
(
module_data
[
"module_yaml"
][
"description"
])
else
:
desc_label
.
set_text
(
module_data
.
get
(
"description"
,
""
))
card_box
.
append
(
desc_label
)
# Кол-во звёзд
stars_label
=
Gtk
.
Label
()
stars_label
.
set_xalign
(
0.0
)
stars_label
.
set_text
(
f
"Stars: {module_data['stars']}"
)
card_box
.
append
(
stars_label
)
# Кнопка “Установить”
install_button
=
Gtk
.
Button
(
label
=
"Установить"
)
install_button
.
connect
(
"clicked"
,
self
.
on_install_clicked
)
card_box
.
append
(
install_button
)
# Ссылка на репозиторий
link_repo_button
=
Gtk
.
LinkButton
.
new_with_label
(
module_data
[
"html_url"
],
"Открыть репозиторий"
)
card_box
.
append
(
link_repo_button
)
def
on_install_clicked
(
self
,
button
):
"""Обработчик нажатия “Установить”. Запускает скачивание и установку в другом потоке."""
def
worker
():
download_and_install_repo
(
self
.
module_data
[
"source"
],
self
.
module_data
[
"repo_full_name"
],
self
.
module_data
[
"repo_name"
],
self
.
module_data
[
"branch"
]
)
# TODO: Показать уведомление в UI, что установка завершена
threading
.
Thread
(
target
=
worker
,
daemon
=
True
)
.
start
()
###############################################################################
# Инициализация Shop Stack
###############################################################################
def
init_shop_stack
(
shop_pagestack
,
shop_listbox
,
shop_split_view
):
"""
Инициализация стека страницы “Магазин”.
"""
refresh_button
=
Gtk
.
Button
(
label
=
"Принудительно обновить список"
)
refresh_button
.
connect
(
"clicked"
,
on_refresh_button_clicked
)
# Индикатор загрузки
spinner
=
Gtk
.
Spinner
()
spinner
.
set_size_request
(
24
,
24
)
# В боксе горизонтально разместим кнопку и спиннер
hbox_top
=
Gtk
.
Box
(
orientation
=
Gtk
.
Orientation
.
HORIZONTAL
,
spacing
=
6
)
hbox_top
.
append
(
refresh_button
)
hbox_top
.
append
(
spinner
)
# Создадим FlowBox для карточек, по 3 в ряд
flowbox
=
Gtk
.
FlowBox
()
flowbox
.
set_vexpand
(
True
)
# Растянем по вертикали
flowbox
.
set_min_children_per_line
(
3
)
flowbox
.
set_max_children_per_line
(
3
)
flowbox
.
set_selection_mode
(
Gtk
.
SelectionMode
.
NONE
)
# Включим только вертикальный scroller
scroller
=
Gtk
.
ScrolledWindow
()
scroller
.
set_child
(
flowbox
)
# Содержимое страницы
vbox_main
=
Gtk
.
Box
(
orientation
=
Gtk
.
Orientation
.
VERTICAL
,
spacing
=
6
)
vbox_main
.
append
(
hbox_top
)
vbox_main
.
append
(
scroller
)
# Показываем в стеке
shop_pagestack
.
add_child
(
vbox_main
)
# TODO: Вынести это отсюда
shop_pagestack
.
_flowbox
=
flowbox
shop_pagestack
.
_spinner
=
spinner
shop_pagestack
.
_current_page
=
1
shop_pagestack
.
_per_page
=
6
shop_pagestack
.
_all_data
=
[]
shop_pagestack
.
_loading
=
False
shop_pagestack
.
_all_loaded
=
False
# Подключим сигнал прокрутки для ленивой подгрузки (пагинации)
scroller
.
connect
(
"edge-reached"
,
on_edge_reached
)
# Начальная загрузка данных
def
load_data_initial
():
load_data
(
shop_pagestack
,
force_refresh
=
False
,
page
=
1
)
GLib
.
idle_add
(
load_data_initial
)
def
on_refresh_button_clicked
(
button
):
"""Обработчик нажатия кнопки “Принудительно обновить список”."""
split_view
=
button
.
get_ancestor
(
Gtk
.
Widget
)
while
split_view
and
not
hasattr
(
split_view
,
"_flowbox"
):
split_view
=
split_view
.
get_parent
()
if
not
split_view
:
return
load_data
(
split_view
,
force_refresh
=
True
,
page
=
1
)
def
on_edge_reached
(
scrolled_window
,
pos
):
"""
edge-reached срабатывает, когда прокрутка достигает края
"""
if
pos
==
Gtk
.
PositionType
.
BOTTOM
:
load_next_page
(
scrolled_window
)
def
load_next_page
(
split_view
):
"""Загружает следующую страницу"""
if
split_view
.
_all_loaded
:
return
if
split_view
.
_loading
:
return
next_page
=
split_view
.
_current_page
+
1
load_data
(
split_view
,
force_refresh
=
False
,
page
=
next_page
)
def
load_data
(
split_view
,
force_refresh
=
False
,
page
=
1
):
"""
Загружает данные.
Если force_refresh=True, то сбрасывает всё и грузит заново.
"""
if
split_view
.
_loading
:
return
split_view
.
_loading
=
True
split_view
.
_spinner
.
start
()
# TODO: Не работает на мета информации, передалать
# Если это новая загрузка (page=1 или force_refresh), чистим flowbox и список
# if page == 1 or force_refresh:
# split_view._flowbox.foreach(lambda child: split_view._flowbox.remove(child))
# split_view._all_data = []
# split_view._all_loaded = False
def
worker
():
try
:
repos
=
retrieve_modules_full_list
(
force_refresh
=
force_refresh
,
initial_page
=
page
,
per_page
=
split_view
.
_per_page
)
except
Exception
as
e
:
repos
=
[]
# Имитируем, что мы действительно получили только часть данных:
start_idx
=
(
page
-
1
)
*
split_view
.
_per_page
end_idx
=
start_idx
+
split_view
.
_per_page
page_data
=
repos
[
start_idx
:
end_idx
]
# Если page_data меньше per_page, значит всё
if
len
(
page_data
)
<
split_view
.
_per_page
:
split_view
.
_all_loaded
=
True
# Дополняем общий список
split_view
.
_all_data
.
extend
(
page_data
)
# Обновляем UI в основном потоке
def
update_ui
():
for
repo
in
page_data
:
card
=
ModuleCard
(
repo
)
split_view
.
_flowbox
.
insert
(
card
,
-
1
)
split_view
.
_current_page
=
page
split_view
.
_spinner
.
stop
()
split_view
.
_loading
=
False
return
False
# чтобы GLib.idle_add один раз выполнилось
GLib
.
idle_add
(
update_ui
)
threading
.
Thread
(
target
=
worker
,
daemon
=
True
)
.
start
()
src/shop/main.py
0 → 100644
View file @
541c7426
import
os
import
shutil
import
tempfile
import
threading
import
requests
import
time
import
yaml
import
gi
gi
.
require_version
(
"Gtk"
,
"4.0"
)
gi
.
require_version
(
"Adw"
,
"1"
)
from
gi.repository
import
Gtk
,
Adw
,
Gdk
,
GObject
,
GLib
## ToDo: Унести это в адаптеры для работы с API
GITHUB_API_URL
=
"https://api.github.com/search/repositories"
GITLAB_API_URL
=
"https://gitlab.com/api/v4/projects"
CACHE_DIR
=
os
.
path
.
join
(
os
.
path
.
expanduser
(
"~"
),
".cache"
,
"tuneit"
,
"shop"
)
if
not
os
.
path
.
exists
(
CACHE_DIR
):
os
.
makedirs
(
CACHE_DIR
,
exist_ok
=
True
)
MODULES_DIR
=
os
.
path
.
join
(
os
.
path
.
expanduser
(
"~"
),
".local"
,
"share"
,
"tuneit"
,
"modules"
)
if
not
os
.
path
.
exists
(
MODULES_DIR
):
os
.
makedirs
(
MODULES_DIR
,
exist_ok
=
True
)
###############################################################################
# Работа с репами и кеш
###############################################################################
def
load_from_cache
():
cache_file
=
os
.
path
.
join
(
CACHE_DIR
,
"modules_cache.json"
)
if
not
os
.
path
.
exists
(
cache_file
):
return
None
,
0
with
open
(
cache_file
,
"r"
,
encoding
=
"utf-8"
)
as
f
:
import
json
data
=
json
.
load
(
f
)
return
data
[
"repos"
],
data
[
"last_update"
]
def
save_to_cache
(
repos
):
cache_file
=
os
.
path
.
join
(
CACHE_DIR
,
"modules_cache.json"
)
with
open
(
cache_file
,
"w"
,
encoding
=
"utf-8"
)
as
f
:
import
json
json
.
dump
({
"last_update"
:
int
(
time
.
time
()),
"repos"
:
repos
},
f
,
ensure_ascii
=
False
,
indent
=
2
)
def
fetch_repos_from_github
(
search_query
,
page
=
1
,
per_page
=
30
):
# Пример https://api.github.com/search/repositories?q=tuneit-mod+in:name
params
=
{
"q"
:
search_query
+
" in:name"
,
"sort"
:
"stars"
,
"order"
:
"desc"
,
"page"
:
page
,
"per_page"
:
per_page
}
r
=
requests
.
get
(
GITHUB_API_URL
,
params
=
params
,
timeout
=
10
)
r
.
raise_for_status
()
data
=
r
.
json
()
return
data
.
get
(
"items"
,
[])
def
fetch_repos_from_gitlab
(
search_query
,
page
=
1
,
per_page
=
30
):
# Пример https://gitlab.com/api/v4/projects?search=tuneit-mod
params
=
{
"search"
:
search_query
,
"order_by"
:
"star_count"
,
"sort"
:
"desc"
,
"page"
:
page
,
"per_page"
:
per_page
}
r
=
requests
.
get
(
GITLAB_API_URL
,
params
=
params
,
timeout
=
10
)
r
.
raise_for_status
()
data
=
r
.
json
()
return
data
def
parse_module_yaml
(
raw_url
):
try
:
r
=
requests
.
get
(
raw_url
,
timeout
=
10
)
r
.
raise_for_status
()
return
yaml
.
safe_load
(
r
.
text
)
except
:
return
None
def
retrieve_modules_full_list
(
force_refresh
=
False
,
initial_page
=
1
,
per_page
=
30
):
"""
Реализует загрузку и логику суточного кеша
"""
# 1. Проверяем кеш
cached_repos
,
last_update
=
load_from_cache
()
day_in_seconds
=
24
*
60
*
60
now
=
int
(
time
.
time
())
# 2. Если кеш валиден и нет force_refresh
if
(
cached_repos
is
not
None
)
and
(
not
force_refresh
)
and
(
now
-
last_update
<
day_in_seconds
):
return
cached_repos
# 3. Иначе загружаем данные
aggregated_repos
=
[]
# GitHub
github_items
=
fetch_repos_from_github
(
"tuneit-mod"
,
page
=
initial_page
,
per_page
=
per_page
)
for
item
in
github_items
:
# Проверяем, что репозиторий (item["name"]) действительно начинается с tuneit-mod
if
not
item
[
"name"
]
.
startswith
(
"tuneit-mod"
):
continue
# Пробуем скачать module.yaml
print
(
item
)
raw_url
=
f
"https://raw.githubusercontent.com/{item['owner']['login']}/{item['name']}/{item['default_branch']}/module.yaml"
module_info
=
parse_module_yaml
(
raw_url
)
aggregated_repos
.
append
({
"source"
:
"github"
,
"repo_name"
:
item
[
"name"
],
"repo_full_name"
:
item
[
"full_name"
],
"stars"
:
item
[
"stargazers_count"
],
"branch"
:
item
[
"default_branch"
],
"description"
:
item
[
"description"
]
or
""
,
"html_url"
:
item
[
"html_url"
],
"star_url"
:
item
[
"html_url"
]
+
"/stargazers"
,
"module_yaml"
:
module_info
[
0
]
})
# GitLab
gitlab_items
=
fetch_repos_from_gitlab
(
"tuneit-mod"
,
page
=
initial_page
,
per_page
=
per_page
)
for
item
in
gitlab_items
:
if
not
item
[
"name"
]
.
startswith
(
"tuneit-mod"
):
continue
# Пробуем скачать module.yaml
namespace_and_repo
=
item
[
"path_with_namespace"
]
# "username/reponame"
raw_url
=
f
"https://gitlab.com/{namespace_and_repo}/-/raw/{item['default_branch']}/module.yaml"
module_info
=
parse_module_yaml
(
raw_url
)
aggregated_repos
.
append
({
"source"
:
"gitlab"
,
"repo_name"
:
item
[
"name"
],
"repo_full_name"
:
namespace_and_repo
,
"branch"
:
item
[
"default_branch"
],
"stars"
:
item
[
"star_count"
],
"description"
:
item
[
"description"
]
or
""
,
"html_url"
:
item
[
"web_url"
],
"star_url"
:
item
[
"web_url"
]
+
"/-/starrers"
,
"module_yaml"
:
module_info
[
0
]
})
# Сохраняем в кеш
save_to_cache
(
aggregated_repos
)
return
aggregated_repos
###############################################################################
# Логика UI
###############################################################################
def
download_and_install_repo
(
source
,
repo_full_name
,
repo_name
,
branch
):
"""
Скачивает ZIP-архив репозитория и распаковывает его
в папку ~/.local/share/tuneit/modules/<repo_name>.
"""
if
source
==
"github"
:
url
=
f
"https://github.com/{repo_full_name}/archive/refs/heads/{branch}.zip"
else
:
# GitLab:
url
=
f
"https://gitlab.com/{repo_full_name}/-/archive/{branch}/{repo_name}.zip"
# Временная папка
tmp_dir
=
tempfile
.
mkdtemp
()
zip_path
=
os
.
path
.
join
(
tmp_dir
,
f
"{repo_name}.zip"
)
try
:
r
=
requests
.
get
(
url
,
stream
=
True
)
r
.
raise_for_status
()
with
open
(
zip_path
,
"wb"
)
as
f
:
for
chunk
in
r
.
iter_content
(
chunk_size
=
8192
):
f
.
write
(
chunk
)
# Распакуем zip
import
zipfile
with
zipfile
.
ZipFile
(
zip_path
,
"r"
)
as
zip_ref
:
zip_ref
.
extractall
(
tmp_dir
)
# Найдём распакованную папку вида <repo_name>-main или что-то вроде
extracted_dirs
=
[
d
for
d
in
os
.
listdir
(
tmp_dir
)
if
os
.
path
.
isdir
(
os
.
path
.
join
(
tmp_dir
,
d
))
and
d
.
startswith
(
repo_name
)]
if
extracted_dirs
:
extracted_path
=
os
.
path
.
join
(
tmp_dir
,
extracted_dirs
[
0
])
# Создаём папку назначения
dest_path
=
os
.
path
.
join
(
MODULES_DIR
,
repo_name
)
# Если уже была папка, удалим её
if
os
.
path
.
exists
(
dest_path
):
shutil
.
rmtree
(
dest_path
)
shutil
.
move
(
extracted_path
,
dest_path
)
finally
:
shutil
.
rmtree
(
tmp_dir
,
ignore_errors
=
True
)
###############################################################################
# Виджет “карточка” модуля
###############################################################################
class
ModuleCard
(
Gtk
.
Box
):
"""
Отображает карточку модуля.
"""
__gtype_name__
=
"ModuleCard"
def
__init__
(
self
,
module_data
):
super
()
.
__init__
(
orientation
=
Gtk
.
Orientation
.
VERTICAL
,
spacing
=
6
)
self
.
module_data
=
module_data
# Настраиваем стили и отступы
self
.
set_margin_top
(
12
)
self
.
set_margin_bottom
(
12
)
self
.
set_margin_start
(
12
)
self
.
set_margin_end
(
12
)
# Фрейм или карточка
frame
=
Gtk
.
Frame
()
frame
.
set_margin_top
(
6
)
frame
.
set_margin_bottom
(
6
)
frame
.
set_margin_start
(
6
)
frame
.
set_margin_end
(
6
)
self
.
append
(
frame
)
card_box
=
Gtk
.
Box
(
orientation
=
Gtk
.
Orientation
.
VERTICAL
,
spacing
=
6
)
card_box
.
set_margin_top
(
6
)
card_box
.
set_margin_bottom
(
6
)
card_box
.
set_margin_start
(
6
)
card_box
.
set_margin_end
(
6
)
frame
.
set_child
(
card_box
)
# Обложка (cover)
cover_url
=
""
if
module_data
[
"module_yaml"
]:
cover_url
=
module_data
[
"module_yaml"
]
.
get
(
"cover"
,
""
)
if
cover_url
:
# TODO: Загрузка обложки в кеш и отображение
try
:
texture
=
Gdk
.
Texture
.
new_for_uri
(
cover_url
)
cover_image
=
Gtk
.
Image
.
new_from_paintable
(
texture
)
cover_image
.
set_size_request
(
200
,
150
)
# 800x600 – размер обложки
card_box
.
append
(
cover_image
)
except
:
pass
title_label
=
Gtk
.
Label
()
if
module_data
[
"module_yaml"
]
and
module_data
[
"module_yaml"
]
.
get
(
"name"
):
title_label
.
set_label
(
module_data
[
"module_yaml"
][
"name"
])
else
:
title_label
.
set_label
(
module_data
[
"repo_name"
])
title_label
.
set_xalign
(
0.0
)
title_label
.
set_markup
(
f
"<b>{title_label.get_label()}</b>"
)
card_box
.
append
(
title_label
)
# Описание
desc_label
=
Gtk
.
Label
()
desc_label
.
set_xalign
(
0.0
)
desc_label
.
set_wrap
(
True
)
if
module_data
[
"module_yaml"
]
and
module_data
[
"module_yaml"
]
.
get
(
"description"
):
desc_label
.
set_text
(
module_data
[
"module_yaml"
][
"description"
])
else
:
desc_label
.
set_text
(
module_data
.
get
(
"description"
,
""
))
card_box
.
append
(
desc_label
)
# Кол-во звёзд
stars_label
=
Gtk
.
Label
()
stars_label
.
set_xalign
(
0.0
)
stars_label
.
set_text
(
f
"Stars: {module_data['stars']}"
)
card_box
.
append
(
stars_label
)
# Кнопка “Установить”
install_button
=
Gtk
.
Button
(
label
=
"Установить"
)
install_button
.
connect
(
"clicked"
,
self
.
on_install_clicked
)
card_box
.
append
(
install_button
)
# Ссылка на репозиторий
link_repo_button
=
Gtk
.
LinkButton
.
new_with_label
(
module_data
[
"html_url"
],
"Открыть репозиторий"
)
card_box
.
append
(
link_repo_button
)
def
on_install_clicked
(
self
,
button
):
"""Обработчик нажатия “Установить”. Запускает скачивание и установку в другом потоке."""
def
worker
():
download_and_install_repo
(
self
.
module_data
[
"source"
],
self
.
module_data
[
"repo_full_name"
],
self
.
module_data
[
"repo_name"
],
self
.
module_data
[
"branch"
]
)
# TODO: Показать уведомление в UI, что установка завершена
threading
.
Thread
(
target
=
worker
,
daemon
=
True
)
.
start
()
###############################################################################
# Инициализация Shop Stack
###############################################################################
def
init_shop_stack
(
shop_pagestack
,
shop_listbox
,
shop_split_view
):
"""
Инициализация стека страницы “Магазин”.
"""
refresh_button
=
Gtk
.
Button
(
label
=
"Принудительно обновить список"
)
refresh_button
.
connect
(
"clicked"
,
on_refresh_button_clicked
)
# Индикатор загрузки
spinner
=
Gtk
.
Spinner
()
spinner
.
set_size_request
(
24
,
24
)
# В боксе горизонтально разместим кнопку и спиннер
hbox_top
=
Gtk
.
Box
(
orientation
=
Gtk
.
Orientation
.
HORIZONTAL
,
spacing
=
6
)
hbox_top
.
append
(
refresh_button
)
hbox_top
.
append
(
spinner
)
# Создадим FlowBox для карточек, по 3 в ряд
flowbox
=
Gtk
.
FlowBox
()
flowbox
.
set_vexpand
(
True
)
# Растянем по вертикали
flowbox
.
set_min_children_per_line
(
3
)
flowbox
.
set_max_children_per_line
(
3
)
flowbox
.
set_selection_mode
(
Gtk
.
SelectionMode
.
NONE
)
# Включим только вертикальный scroller
scroller
=
Gtk
.
ScrolledWindow
()
scroller
.
set_child
(
flowbox
)
# Содержимое страницы
vbox_main
=
Gtk
.
Box
(
orientation
=
Gtk
.
Orientation
.
VERTICAL
,
spacing
=
6
)
vbox_main
.
append
(
hbox_top
)
vbox_main
.
append
(
scroller
)
# Показываем в стеке
shop_pagestack
.
add_child
(
vbox_main
)
# TODO: Вынести это отсюда
shop_pagestack
.
_flowbox
=
flowbox
shop_pagestack
.
_spinner
=
spinner
shop_pagestack
.
_current_page
=
1
shop_pagestack
.
_per_page
=
6
shop_pagestack
.
_all_data
=
[]
shop_pagestack
.
_loading
=
False
shop_pagestack
.
_all_loaded
=
False
# Подключим сигнал прокрутки для ленивой подгрузки (пагинации)
scroller
.
connect
(
"edge-reached"
,
on_edge_reached
)
# Начальная загрузка данных
def
load_data_initial
():
load_data
(
shop_pagestack
,
force_refresh
=
False
,
page
=
1
)
GLib
.
idle_add
(
load_data_initial
)
def
on_refresh_button_clicked
(
button
):
"""Обработчик нажатия кнопки “Принудительно обновить список”."""
split_view
=
button
.
get_ancestor
(
Gtk
.
Widget
)
while
split_view
and
not
hasattr
(
split_view
,
"_flowbox"
):
split_view
=
split_view
.
get_parent
()
if
not
split_view
:
return
load_data
(
split_view
,
force_refresh
=
True
,
page
=
1
)
def
on_edge_reached
(
scrolled_window
,
pos
):
"""
edge-reached срабатывает, когда прокрутка достигает края
"""
if
pos
==
Gtk
.
PositionType
.
BOTTOM
:
load_next_page
(
scrolled_window
)
def
load_next_page
(
split_view
):
"""Загружает следующую страницу"""
if
split_view
.
_all_loaded
:
return
if
split_view
.
_loading
:
return
next_page
=
split_view
.
_current_page
+
1
load_data
(
split_view
,
force_refresh
=
False
,
page
=
next_page
)
def
load_data
(
split_view
,
force_refresh
=
False
,
page
=
1
):
"""
Загружает данные.
Если force_refresh=True, то сбрасывает всё и грузит заново.
"""
if
split_view
.
_loading
:
return
split_view
.
_loading
=
True
split_view
.
_spinner
.
start
()
# TODO: Не работает на мета информации, передалать
# Если это новая загрузка (page=1 или force_refresh), чистим flowbox и список
# if page == 1 or force_refresh:
# split_view._flowbox.foreach(lambda child: split_view._flowbox.remove(child))
# split_view._all_data = []
# split_view._all_loaded = False
def
worker
():
try
:
repos
=
retrieve_modules_full_list
(
force_refresh
=
force_refresh
,
initial_page
=
page
,
per_page
=
split_view
.
_per_page
)
except
Exception
as
e
:
repos
=
[]
# Имитируем, что мы действительно получили только часть данных:
start_idx
=
(
page
-
1
)
*
split_view
.
_per_page
end_idx
=
start_idx
+
split_view
.
_per_page
page_data
=
repos
[
start_idx
:
end_idx
]
# Если page_data меньше per_page, значит всё
if
len
(
page_data
)
<
split_view
.
_per_page
:
split_view
.
_all_loaded
=
True
# Дополняем общий список
split_view
.
_all_data
.
extend
(
page_data
)
# Обновляем UI в основном потоке
def
update_ui
():
for
repo
in
page_data
:
card
=
ModuleCard
(
repo
)
split_view
.
_flowbox
.
insert
(
card
,
-
1
)
split_view
.
_current_page
=
page
split_view
.
_spinner
.
stop
()
split_view
.
_loading
=
False
return
False
# чтобы GLib.idle_add один раз выполнилось
GLib
.
idle_add
(
update_ui
)
threading
.
Thread
(
target
=
worker
,
daemon
=
True
)
.
start
()
src/window.py
View file @
541c7426
...
@@ -21,7 +21,7 @@ import threading
...
@@ -21,7 +21,7 @@ import threading
from
gi.repository
import
GObject
,
Adw
,
Gtk
,
GLib
from
gi.repository
import
GObject
,
Adw
,
Gtk
,
GLib
from
.settings.main
import
init_settings_stack
from
.settings.main
import
init_settings_stack
from
.shop
import
init_shop_stack
from
.shop
.main
import
init_shop_stack
@Gtk.Template
(
resource_path
=
'/ru.ximperlinux.TuneIt/window.ui'
)
@Gtk.Template
(
resource_path
=
'/ru.ximperlinux.TuneIt/window.ui'
)
class
TuneitWindow
(
Adw
.
ApplicationWindow
):
class
TuneitWindow
(
Adw
.
ApplicationWindow
):
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment