Shared KMP core and UI, switch to Koin
This commit is contained in:
81
tools/linux/l0/float_test.py
Normal file
81
tools/linux/l0/float_test.py
Normal file
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env python3
|
||||
"""L0: как обычный mpv ведёт себя плавающим окном в Cinnamon (эталон для L1/L8).
|
||||
|
||||
float_test.py <037|041>
|
||||
Запускает mpv --ontop --no-border --geometry=640x360-24-24 и по IPC переключает режимы,
|
||||
после каждого шага печатает геометрию окна, _NET_WM_STATE, рамку (_NET_FRAME_EXTENTS) и активное окно.
|
||||
"""
|
||||
import json, os, socket, subprocess, sys, time
|
||||
from Xlib import X, display
|
||||
|
||||
# mpv 0.37 из репозитория Mint (или распакованный из .deb без установки: apt download mpv; dpkg -x)
|
||||
M037 = os.environ.get("MPV037", "mpv")
|
||||
RUN = os.path.join(os.environ.get("XDG_RUNTIME_DIR", "/run/user/1000"), "l0")
|
||||
os.makedirs(RUN, exist_ok=True)
|
||||
ver = sys.argv[1]
|
||||
sock = os.path.join(RUN, f"float{ver}.sock")
|
||||
if os.path.exists(sock): os.unlink(sock)
|
||||
item = "00000000000000000000000000000000"
|
||||
url = f"http://192.168.1.10:8096/Videos/{item}/stream?static=true&mediaSourceId={item}"
|
||||
opts = ["--config=no", "--mute=yes", "--start=30%", "--hwdec=auto-safe", "--vo=gpu", "--gpu-api=opengl",
|
||||
"--ontop", "--no-border", "--geometry=640x360-24-24", "--screen=0", "--keep-open=yes",
|
||||
"--title=L0 float", f"--input-ipc-server={sock}"]
|
||||
cmd = ([M037] if ver == "037" else ["flatpak", "run", "--filesystem=xdg-run/l0", "io.mpv.Mpv"]) + opts + [url]
|
||||
|
||||
d = display.Display(); root = d.screen().root
|
||||
A = lambda n: d.intern_atom(n)
|
||||
NET_ACTIVE, NET_STATE, FRAME, NAME = A("_NET_ACTIVE_WINDOW"), A("_NET_WM_STATE"), A("_NET_FRAME_EXTENTS"), A("_NET_WM_NAME")
|
||||
|
||||
def active():
|
||||
p = root.get_full_property(NET_ACTIVE, X.AnyPropertyType)
|
||||
return p.value[0] if p else None
|
||||
|
||||
def find_win():
|
||||
for _ in range(100):
|
||||
p = root.get_full_property(A("_NET_CLIENT_LIST"), X.AnyPropertyType)
|
||||
for wid in (p.value if p else []):
|
||||
w = d.create_resource_object("window", wid)
|
||||
n = w.get_full_property(NAME, 0)
|
||||
if n and b"L0 float" in (n.value if isinstance(n.value, bytes) else n.value.encode()): return w
|
||||
time.sleep(0.1)
|
||||
|
||||
def geom(w):
|
||||
g = w.get_geometry(); t = w.translate_coords(root, 0, 0)
|
||||
st = w.get_full_property(NET_STATE, X.AnyPropertyType)
|
||||
states = [d.get_atom_name(x).replace("_NET_WM_STATE_", "") for x in (st.value if st else [])]
|
||||
fe = w.get_full_property(FRAME, X.AnyPropertyType)
|
||||
return {"x": -t.x, "y": -t.y, "w": g.width, "h": g.height, "state": states,
|
||||
"frame": list(fe.value) if fe else None, "focused": active() == w.id}
|
||||
|
||||
class Ipc:
|
||||
def __init__(self):
|
||||
for _ in range(200):
|
||||
try: self.s = socket.socket(socket.AF_UNIX); self.s.connect(sock); break
|
||||
except OSError: time.sleep(0.05)
|
||||
self.f = self.s.makefile("rb")
|
||||
def cmd(self, *a):
|
||||
self.s.sendall(json.dumps({"command": list(a), "request_id": 1}).encode() + b"\n")
|
||||
while True:
|
||||
m = json.loads(self.f.readline())
|
||||
if m.get("request_id") == 1: return m
|
||||
|
||||
before = active()
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
ipc = Ipc(); w = find_win(); time.sleep(1.5)
|
||||
print("active before launch:", hex(before) if before else None)
|
||||
steps = [("start (ontop, no border)", None),
|
||||
("ontop=no", ("set", "ontop", "no")),
|
||||
("border=yes", ("set", "border", "yes")),
|
||||
("fullscreen=yes", ("set", "fullscreen", "yes")),
|
||||
("fullscreen=no", ("set", "fullscreen", "no")),
|
||||
("border=no + ontop=yes", ("set", "border", "no")),
|
||||
("screen=1 portrait (geometry 640x360-24-24)", None),
|
||||
("resize 960x540 via geometry", ("set", "geometry", "960x540-24-24"))]
|
||||
for label, c in steps:
|
||||
if c: ipc.cmd(*c)
|
||||
if label.startswith("border=no"): ipc.cmd("set", "ontop", "yes")
|
||||
if label.startswith("screen=1"):
|
||||
ipc.cmd("set", "screen", "1"); ipc.cmd("set", "geometry", "640x360-24-24")
|
||||
time.sleep(1.2)
|
||||
print(f"{label:45s}", json.dumps(geom(w)))
|
||||
ipc.cmd("quit"); proc.wait(timeout=10)
|
||||
15
tools/linux/l0/run_all.sh
Executable file
15
tools/linux/l0/run_all.sh
Executable file
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
# L0 (LINUX_PLAN.md §3, итоги §6). Нужны: mpv 0.37 ($MPV037 или mpv в PATH), Flatpak io.mpv.Mpv (0.41), python3-xlib для float_test.py.
|
||||
# Прогон всех файлов: 0.37 и 0.41 в vo=gpu (как render API), HDR/DV ещё в gpu-next 0.41, чересстрочные с деинтерлейсом.
|
||||
D=$(dirname "$0"); OUT=${L0_OUT:-${XDG_RUNTIME_DIR:-/tmp}/l0/out}; mkdir -p $OUT; R=$OUT/results.jsonl
|
||||
run() { timeout 150 python3 -I $D/run_case.py "$@" --play 15 >> $R || echo "{\"name\":\"$4\",\"ver\":\"$1\",\"vo\":\"$2\",\"error\":\"timeout\"}" >> $R; }
|
||||
while read -r name id rest; do
|
||||
for ver in 037 041; do run $ver gpu $id $name $rest; done
|
||||
case $name in dv*|hlg*|hdr*|av1_4k*) run 041 gpu-next $id $name $rest;; esac
|
||||
case $name in dvd_ps_576i|h264_1080i50)
|
||||
run 037 gpu $id ${name}_deint --extra=--deinterlace=yes
|
||||
run 041 gpu $id ${name}_deint --extra=--deinterlace=yes
|
||||
run 041 gpu $id ${name}_deintauto --extra=--deinterlace=auto;;
|
||||
esac
|
||||
done < $D/cases.txt
|
||||
echo DONE
|
||||
209
tools/linux/l0/run_case.py
Normal file
209
tools/linux/l0/run_case.py
Normal file
@@ -0,0 +1,209 @@
|
||||
#!/usr/bin/env python3
|
||||
"""L0: один прогон файла в mpv с замерами через IPC.
|
||||
|
||||
run_case.py <mpv: 037|041> <vo: gpu|gpu-next> <item_id> <name> [--sid N] [--aid N] [--sub-url URL]
|
||||
[--start SEC] [--play SEC] [--seek SEC] [--extra "--opt=val ..."]
|
||||
Печатает одну строку JSON с результатами в stdout.
|
||||
"""
|
||||
import argparse, json, os, shlex, socket, subprocess, sys, threading, time
|
||||
|
||||
OUT = os.environ.get("L0_OUT") or os.path.join(os.environ.get("XDG_RUNTIME_DIR", "/tmp"), "l0", "out")
|
||||
# mpv 0.37 из репозитория Mint (или распакованный из .deb без установки: apt download mpv; dpkg -x)
|
||||
M037 = os.environ.get("MPV037", "mpv")
|
||||
SERVER = "http://192.168.1.10:8096"
|
||||
# Токен-заглушка: сервер отдаёт static-поток и без токена; проверяем, что заголовок доходит и не ломает запрос.
|
||||
HEADER = 'Authorization: MediaBrowser Token="0123456789abcdef0123456789abcdef"'
|
||||
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument("ver"); p.add_argument("vo"); p.add_argument("item"); p.add_argument("name")
|
||||
p.add_argument("--sid"); p.add_argument("--aid"); p.add_argument("--sub-url")
|
||||
p.add_argument("--start", type=float); p.add_argument("--play", type=float, default=20)
|
||||
p.add_argument("--seek", type=float, default=300); p.add_argument("--extra", default="")
|
||||
a = p.parse_args()
|
||||
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
tag = f"{a.name}_{a.ver}_{a.vo}"
|
||||
RUN = os.path.join(os.environ.get("XDG_RUNTIME_DIR", "/run/user/1000"), "l0")
|
||||
os.makedirs(RUN, exist_ok=True)
|
||||
sock = os.path.join(RUN, f"{a.ver}{a.vo}.sock") # путь сокета ≤ 108 символов
|
||||
log = os.path.join(OUT, f"{tag}.log")
|
||||
shot = os.path.join(OUT, f"{tag}.png")
|
||||
for f in (sock, log, shot):
|
||||
if os.path.exists(f): os.unlink(f)
|
||||
|
||||
url = f"{SERVER}/Videos/{a.item}/stream?static=true&mediaSourceId={a.item}"
|
||||
opts = [
|
||||
"--config=no", "--idle=no", "--force-window=yes", "--keep-open=yes",
|
||||
"--screen=0", "--geometry=640x360-24-24", f"--title=L0 {tag}",
|
||||
f"--vo={a.vo}", "--gpu-api=opengl", "--gpu-context=x11egl", "--hwdec=auto-safe",
|
||||
"--mute=yes", "--ao=pipewire,pulse,alsa", "--audio-channels=auto-safe",
|
||||
"--hr-seek=yes", "--cache=yes", "--demuxer-max-bytes=150MiB", "--demuxer-max-back-bytes=50MiB",
|
||||
"--osc=no", "--input-default-bindings=no", "--ytdl=no", "--tone-mapping=auto",
|
||||
f"--http-header-fields={HEADER}",
|
||||
f"--input-ipc-server={sock}", f"--log-file={log}",
|
||||
"--screenshot-format=png", "--screenshot-png-compression=1",
|
||||
]
|
||||
if a.vo == "gpu-next":
|
||||
opts = [o for o in opts if not o.startswith("--gpu-api") and not o.startswith("--gpu-context")]
|
||||
if a.start is not None: opts.append(f"--start={a.start}")
|
||||
else: opts.append("--start=40%")
|
||||
if a.sid: opts.append(f"--sid={a.sid}")
|
||||
if a.aid: opts.append(f"--aid={a.aid}")
|
||||
if a.sub_url: opts += [f"--sub-files-append={a.sub_url}", "--sid=auto"]
|
||||
opts += shlex.split(a.extra)
|
||||
|
||||
if a.ver == "037":
|
||||
cmd = [M037] + opts + [url]
|
||||
else:
|
||||
cmd = ["flatpak", "run", f"--filesystem={OUT}", "--filesystem=xdg-run/l0", "io.mpv.Mpv"] + opts + [url]
|
||||
|
||||
t_launch = time.time()
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
|
||||
def find_pid():
|
||||
# Хост-PID процесса mpv (во Flatpak он не совпадает с pid из IPC)
|
||||
for _ in range(100):
|
||||
out = subprocess.run(["pgrep", "-f", sock], capture_output=True, text=True).stdout.split()
|
||||
for pid in out:
|
||||
try:
|
||||
if open(f"/proc/{pid}/comm").read().strip() in ("mpv", "mpv-bin"): return int(pid)
|
||||
except OSError: pass
|
||||
time.sleep(0.05)
|
||||
return None
|
||||
|
||||
class Ipc:
|
||||
def __init__(self, path):
|
||||
for _ in range(200):
|
||||
try:
|
||||
self.s = socket.socket(socket.AF_UNIX); self.s.connect(path); break
|
||||
except OSError: time.sleep(0.05)
|
||||
else: raise RuntimeError("no ipc")
|
||||
self.buf = b""; self.rid = 0; self.lock = threading.Lock()
|
||||
def cmd(self, *args):
|
||||
with self.lock:
|
||||
self.rid += 1; rid = self.rid
|
||||
self.s.sendall(json.dumps({"command": list(args), "request_id": rid}).encode() + b"\n")
|
||||
while True:
|
||||
while b"\n" not in self.buf:
|
||||
d = self.s.recv(65536)
|
||||
if not d: raise RuntimeError("ipc closed")
|
||||
self.buf += d
|
||||
line, self.buf = self.buf.split(b"\n", 1)
|
||||
m = json.loads(line)
|
||||
if m.get("request_id") == rid: return m.get("data") if m.get("error") == "success" else None
|
||||
def get(self, prop): return self.cmd("get_property", prop)
|
||||
|
||||
def proc_stats(pid):
|
||||
try:
|
||||
st = open(f"/proc/{pid}/stat").read().rsplit(")", 1)[1].split()
|
||||
cpu = (int(st[11]) + int(st[12])) / os.sysconf("SC_CLK_TCK")
|
||||
except OSError: cpu = None
|
||||
try:
|
||||
io = dict(l.split(": ") for l in open(f"/proc/{pid}/io").read().splitlines())
|
||||
rchar = int(io["rchar"])
|
||||
except (OSError, KeyError): rchar = None
|
||||
return cpu, rchar
|
||||
|
||||
def gpu_sampler(stop, samples):
|
||||
q = subprocess.Popen(["nvidia-smi", "--query-gpu=utilization.gpu,utilization.decoder",
|
||||
"--format=csv,noheader,nounits", "-lms", "250"],
|
||||
stdout=subprocess.PIPE, text=True)
|
||||
while not stop.is_set():
|
||||
line = q.stdout.readline()
|
||||
if not line: break
|
||||
try: samples.append([int(x) for x in line.split(",")])
|
||||
except ValueError: pass
|
||||
q.terminate()
|
||||
|
||||
res = {"name": a.name, "ver": a.ver, "vo": a.vo, "item": a.item}
|
||||
try:
|
||||
ipc = Ipc(sock)
|
||||
pid = find_pid()
|
||||
# Первый кадр: VO получил параметры и время пошло
|
||||
first = None
|
||||
for _ in range(600):
|
||||
if ipc.get("video-out-params") and ipc.get("time-pos") is not None:
|
||||
first = time.time() - t_launch; break
|
||||
if proc.poll() is not None: break
|
||||
time.sleep(0.05)
|
||||
res["first_frame_s"] = round(first, 2) if first else None
|
||||
|
||||
def counters():
|
||||
return {k: ipc.get(k) for k in ("frame-drop-count", "decoder-frame-drop-count",
|
||||
"vo-delayed-frame-count", "mistimed-frame-count")}
|
||||
def window(sec, label):
|
||||
global pid
|
||||
time.sleep(1.5)
|
||||
if pid is None: pid = find_pid() # Flatpak стартует дольше, чем ждёт find_pid
|
||||
c0 = counters(); cpu0, r0 = proc_stats(pid); t0 = time.time(); pos0 = ipc.get("time-pos") or 0
|
||||
stop = threading.Event(); gs = []
|
||||
th = threading.Thread(target=gpu_sampler, args=(stop, gs)); th.start()
|
||||
av, pauses, was_paused, speeds = [], 0, False, []
|
||||
while time.time() - t0 < sec:
|
||||
cs = ipc.get("cache-speed")
|
||||
if cs is not None: speeds.append(cs)
|
||||
v = ipc.get("avsync")
|
||||
if v is not None: av.append(abs(v))
|
||||
pc = bool(ipc.get("paused-for-cache"))
|
||||
if pc and not was_paused: pauses += 1
|
||||
was_paused = pc
|
||||
time.sleep(0.2)
|
||||
stop.set(); th.join()
|
||||
c1 = counters(); cpu1, r1 = proc_stats(pid); dt = time.time() - t0; pos1 = ipc.get("time-pos") or 0
|
||||
w = {k.replace("-count", ""): (c1[k] - c0[k]) if c0[k] is not None and c1[k] is not None else None for k in c0}
|
||||
w["played_s"] = round(pos1 - pos0, 1); w["wall_s"] = round(dt, 1)
|
||||
w["cpu_pct"] = round(100 * (cpu1 - cpu0) / dt) if cpu0 is not None and cpu1 is not None else None
|
||||
w["net_mbit"] = round(sum(speeds) * 8 / len(speeds) / 1e6, 1) if speeds else None
|
||||
w["avsync_max_ms"] = round(1000 * max(av), 1) if av else None
|
||||
w["cache_pauses"] = pauses
|
||||
if gs:
|
||||
w["gpu_pct"] = round(sum(x[0] for x in gs) / len(gs)); w["nvdec_pct"] = round(sum(x[1] for x in gs) / len(gs))
|
||||
res[label] = w
|
||||
|
||||
window(a.play, "play")
|
||||
info = {}
|
||||
for k in ("hwdec-current", "video-codec", "video-format", "container-fps", "estimated-vf-fps", "display-fps",
|
||||
"audio-codec-name", "current-ao", "seekable", "duration", "file-format", "vf"):
|
||||
info[k] = ipc.get(k)
|
||||
vp = ipc.get("video-params") or {}
|
||||
info["video-params"] = {k: vp.get(k) for k in ("pixelformat", "w", "h", "colormatrix", "primaries", "gamma", "sig-peak", "light")}
|
||||
vop = ipc.get("video-out-params") or {}
|
||||
info["video-out-params"] = {k: vop.get(k) for k in ("pixelformat", "gamma", "primaries", "sig-peak")}
|
||||
ap = ipc.get("audio-params") or {}
|
||||
info["audio-params"] = {k: ap.get(k) for k in ("format", "samplerate", "channel-count", "hr-channels")}
|
||||
tl = ipc.get("track-list") or []
|
||||
info["tracks"] = [f"{t['type'][0]}{t['id']}{'*' if t.get('selected') else ''}:{t.get('codec')}{'(ext)' if t.get('external') else ''}" for t in tl]
|
||||
res["info"] = info
|
||||
ipc.cmd("screenshot-to-file", shot, "window")
|
||||
|
||||
# Перемотка вперёд (точная, как в приложении)
|
||||
pos = ipc.get("time-pos") or 0
|
||||
t0 = time.time()
|
||||
ipc.cmd("seek", str(a.seek), "relative+exact")
|
||||
seek_done = None
|
||||
time.sleep(0.1)
|
||||
for _ in range(600):
|
||||
tp = ipc.get("time-pos")
|
||||
if not ipc.get("seeking") and not ipc.get("paused-for-cache") and tp is not None and tp > pos + a.seek - 1:
|
||||
# дождаться, что время пошло
|
||||
t1 = ipc.get("time-pos"); time.sleep(0.25); t2 = ipc.get("time-pos")
|
||||
if t2 and t1 and t2 > t1: seek_done = time.time() - t0 - 0.25; break
|
||||
time.sleep(0.05)
|
||||
res["seek_s"] = round(seek_done, 2) if seek_done else None
|
||||
window(10, "after_seek")
|
||||
ipc.cmd("quit")
|
||||
except Exception as e:
|
||||
res["error"] = repr(e)
|
||||
try: proc.wait(timeout=10)
|
||||
except subprocess.TimeoutExpired: proc.kill()
|
||||
|
||||
# Предупреждения и ошибки из лога
|
||||
errs = []
|
||||
try:
|
||||
for line in open(log, errors="replace"):
|
||||
parts = line.split(" ", 3)
|
||||
if "][e][" in line[:24] or "][w][" in line[:24]:
|
||||
errs.append(line.strip()[:200])
|
||||
except OSError: pass
|
||||
res["log_warn"] = errs[:15]; res["log_warn_n"] = len(errs)
|
||||
print(json.dumps(res, ensure_ascii=False))
|
||||
@@ -3,18 +3,22 @@
|
||||
Builds the app's logo resources from the source picture design/logo/sleepyowl.png (owl over the "SleepyOwl"
|
||||
wordmark on a flat navy background, 1254×1254):
|
||||
|
||||
drawable-xhdpi/banner.png Android TV banner, 320×180 dp: owl + wordmark on the logo's navy
|
||||
drawable-xxxhdpi/ic_launcher_owl.png adaptive icon foreground (108 dp), background is @color/ic_launcher_background
|
||||
drawable-xhdpi/logo_owl.png owl alone, transparent — side rail, login, settings
|
||||
drawable-xhdpi/logo_wordmark.png "SleepyOwl" alone, transparent
|
||||
app/src/main/res/drawable-xhdpi/banner.png Android TV banner, 320×180 dp: owl + wordmark on navy
|
||||
app/src/main/res/drawable-xxxhdpi/ic_launcher_owl.png adaptive icon foreground (108 dp), background is
|
||||
@color/ic_launcher_background
|
||||
ui/shared/src/androidMain/res/drawable-xhdpi/logo_owl.png owl alone, transparent — side rail, login, settings
|
||||
ui/shared/src/androidMain/res/drawable-xhdpi/logo_wordmark.png "SleepyOwl" alone, transparent
|
||||
|
||||
The in-app marks belong to ui:shared: the TV uses them as resources, the desktop reads the same files from its
|
||||
classpath.
|
||||
|
||||
The navy is keyed out ("color to alpha"): the glow stays as translucent amber, so the owl and the wordmark sit on the
|
||||
app's own dark background without a square. The owl's body is made opaque so it does not take the colour of what is
|
||||
behind it. The in-app pictures are made at the TV's density (xhdpi, 2 px per dp) and at the size they are drawn
|
||||
(see ui/components/Brand.kt), so they are not resampled on the TV.
|
||||
(see ui/components/Brand.kt in ui:shared), so they are not resampled on the TV.
|
||||
|
||||
Usage:
|
||||
python3 tools/logo/make_assets.py [--src design/logo/sleepyowl.png] [--res app/src/main/res] [--preview DIR]
|
||||
python3 tools/logo/make_assets.py [--src design/logo/sleepyowl.png] [--root REPO] [--preview DIR]
|
||||
"""
|
||||
import argparse
|
||||
import os
|
||||
@@ -27,6 +31,9 @@ BG = np.array([5, 9, 39]) / 255 # the logo's background
|
||||
DEAD_ZONE = 7 / 255 # background noise and vignette, keyed out as well
|
||||
OWL_ROWS = (0, 870) # the owl ends at ~858, the wordmark starts at ~882
|
||||
WORD_ROWS = (870, 1080)
|
||||
# Where the pictures go, relative to the repository: the TV app's own and the shared UI's resources.
|
||||
APP_RES = "app/src/main/res/"
|
||||
SHARED_RES = "ui/shared/src/androidMain/res/"
|
||||
|
||||
DP = 2 # xhdpi
|
||||
|
||||
@@ -107,7 +114,7 @@ def build(src):
|
||||
word_b = fit(word, 32 * DP)
|
||||
gap = 6 * DP
|
||||
x = (bw - owl_b.width - gap - word_b.width) // 2
|
||||
out["drawable-xhdpi/banner.png"] = on_navy(
|
||||
out[APP_RES + "drawable-xhdpi/banner.png"] = on_navy(
|
||||
(bw, bh),
|
||||
(owl_b, (x, (bh - owl_b.height) // 2)),
|
||||
(word_b, (x + owl_b.width + gap, (bh - word_b.height) // 2 + 2 * DP)),
|
||||
@@ -119,11 +126,11 @@ def build(src):
|
||||
owl_i = fit(owl, round(owl.height * scale))
|
||||
fg = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
||||
fg.alpha_composite(owl_i, ((size - owl_i.width) // 2, (size - owl_i.height) // 2))
|
||||
out["drawable-xxxhdpi/ic_launcher_owl.png"] = fg
|
||||
out[APP_RES + "drawable-xxxhdpi/ic_launcher_owl.png"] = fg
|
||||
|
||||
# In-app marks, drawn at exactly these sizes (Brand.kt).
|
||||
out["drawable-xhdpi/logo_owl.png"] = fit(owl, 36 * DP)
|
||||
out["drawable-xhdpi/logo_wordmark.png"] = fit(word, 18 * DP)
|
||||
out[SHARED_RES + "drawable-xhdpi/logo_owl.png"] = fit(owl, 36 * DP)
|
||||
out[SHARED_RES + "drawable-xhdpi/logo_wordmark.png"] = fit(word, 18 * DP)
|
||||
return out
|
||||
|
||||
|
||||
@@ -131,12 +138,12 @@ def main():
|
||||
root = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
p.add_argument("--src", default=os.path.join(root, "design", "logo", "sleepyowl.png"))
|
||||
p.add_argument("--res", default=os.path.join(root, "app", "src", "main", "res"))
|
||||
p.add_argument("--root", default=root, help="repository root the resource paths are relative to")
|
||||
p.add_argument("--preview", help="also write the pictures over the app's background into this directory")
|
||||
args = p.parse_args()
|
||||
|
||||
for path, img in build(args.src).items():
|
||||
dst = os.path.join(args.res, path)
|
||||
dst = os.path.join(args.root, path)
|
||||
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
||||
img.save(dst, optimize=True)
|
||||
print(f"{dst} {img.width}×{img.height}")
|
||||
|
||||
Reference in New Issue
Block a user