Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions src/themeSwitcher/installTheme.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,14 @@ void loadThemeDirectory(const char *theme_dir,
}

if (is_file(config_path)) {
strcpy(themes_out[*count], ep->d_name);
*count += 1;
if (*count >= NUMBER_OF_THEMES) {
printf_debug("Theme limit reached (%d); ignoring remaining entries in %s\n",
NUMBER_OF_THEMES, theme_dir);
break;
}

snprintf(themes_out[*count], STR_MAX, "%s", ep->d_name);
(*count)++;
}
}
closedir(dp);
Expand All @@ -72,8 +78,9 @@ void loadThemeDirectory(const char *theme_dir,

void updatePreviews()
{
system(SCRIPT_DIR "/themes_extract_previews.sh");
sync();
int result = system(SCRIPT_DIR "/themes_extract_previews.sh");
if (result != 0)
printf_debug("Theme preview update script failed: %d\n", result);
}

int listAllThemes(char themes_out[NUMBER_OF_THEMES][STR_MAX], const char *installed_theme, int *installed_page)
Expand Down
105 changes: 93 additions & 12 deletions src/themeSwitcher/script/themes_extract_previews.sh
Original file line number Diff line number Diff line change
@@ -1,10 +1,27 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-3.0-only
# The script scans /mnt/SDCARD/Themes for .zip, .7z, and .rar theme archives,
# then extracts only lightweight preview assets config.json, preview.png, and
# icons/gba.png into /mnt/SDCARD/Themes/.previews.
# It caches each archives size and modification time so unchanged archives
# can be skipped without recalculating MD5. MD5 hashes are used to detect
# already-installed themes or previews extracted from the same archive.
# When a matching theme is already installed, the archive and any redundant
# preview are removed.
# The upstream sync after extraction is deliberately omitted: previews are a
# regenerable cache and syncing here is costly on SD-backed storage.
# Usage: themes_extract_previews.sh

theme_dir=/mnt/SDCARD/Themes
previews_root="$theme_dir/.previews"
archive_cache_dir="$previews_root/.archives"

if [ ! -d "$theme_dir" ]; then
exit 2
fi

mkdir -p "$archive_cache_dir"

main () {
scan_for_archives "zip"
scan_for_archives "7z"
Expand All @@ -25,6 +42,18 @@ check_archive() {
archive_path="$1"
ext="$2"
archive_name="$(basename "$archive_path" ".$ext")"
archive_key="$archive_name.$ext"
archive_stat_path="$archive_cache_dir/$archive_key.stat"
archive_themes_path="$archive_cache_dir/$archive_key.themes"
current_stat=$(archive_stat "$archive_path")

if [ -n "$current_stat" ] && \
archive_cache_valid "$archive_path" "$archive_stat_path" \
"$archive_themes_path" "$current_stat" ; then
echo "[IGNORE] unchanged archive: $archive_name"
return
fi

archive_hash=$(md5hash "$archive_path")

if compare_hash "$theme_dir/$archive_name" "$archive_hash" ; then
Expand All @@ -36,57 +65,78 @@ check_archive() {
rm -rf "$theme_dir/.previews/$archive_name"
fi

rm -f "$archive_stat_path" "$archive_themes_path"
return
fi

if compare_hash "$theme_dir/.previews/$archive_name" "$archive_hash" ; then
echo "[IGNORE] found preview for: $archive_name"
printf '%s\n' "$archive_name" > "$archive_themes_path"
if [ -n "$current_stat" ]; then
printf '%s\n' "$current_stat" > "$archive_stat_path"
fi
return
fi

echo "[CHECK] checking archive: $archive_path"

file_list=`7z l -slt "$archive_path" "*/config.json" | grep '^Path = ' | sed 's/^Path = //g' | tail -n +2 | awk -v prefix="$theme_dir/.previews/" '$0=prefix $0'`
archive_themes_tmp="$archive_themes_path.tmp.$$"
: > "$archive_themes_tmp"

echo "$file_list" | while read line; do
echo "$file_list" | while IFS= read -r line; do
output_dir="$(dirname "$line")"

if [ "$output_dir" == "." ]; then
if [ "$output_dir" = "." ]; then
continue
fi

theme_name="$(basename "$output_dir")"
printf '%s\n' "$theme_name" >> "$archive_themes_tmp"
extract_preview "$output_dir" "$archive_path" "$archive_hash"
done

# Only cache when at least one theme was found. An empty result can mean a
# corrupt or unreadable archive rather than one genuinely containing no
# themes; caching that would skip the archive on every later run until its
# size or modification time changes.
if [ -s "$archive_themes_tmp" ]; then
mv -f "$archive_themes_tmp" "$archive_themes_path"
if [ -n "$current_stat" ]; then
printf '%s\n' "$current_stat" > "$archive_stat_path"
fi
else
rm -f "$archive_themes_tmp"
fi
}

extract_preview() {
preview_dir="$1"
theme_preview_dir="$1"
archive_path="$2"
archive_hash="$3"
theme_name="$(basename "$preview_dir")"
theme_name="$(basename "$theme_preview_dir")"

if compare_hash "$theme_dir/$theme_name" "$archive_hash" ; then
echo " [IGNORE] theme already installed: $theme_name"

if [ -d "$preview_dir" ]; then
rm -rf "$preview_dir"
if [ -d "$theme_preview_dir" ]; then
rm -rf "$theme_preview_dir"
fi

return
fi

if compare_hash "$preview_dir" "$archive_hash" ; then
if compare_hash "$theme_preview_dir" "$archive_hash" ; then
echo " [IGNORE] found preview for: $theme_name"
return
fi

output=`7z x -aoa -bd -bb1 -o"$theme_dir/.previews" "$archive_path" "$theme_name/config.json" "$theme_name/preview.png" "$theme_name/icons/gba.png" | grep '^- .*/config.json$' | sed 's/^- //g' | awk -v prefix="$theme_dir/.previews/" '$0=prefix $0'`
sync

echo "$output" | while read line; do
echo "$output" | while IFS= read -r line; do
output_dir="$(dirname "$line")"

if [ "$output_dir" == "." ]; then
if [ "$output_dir" = "." ]; then
continue
fi

Expand All @@ -96,14 +146,45 @@ extract_preview() {
done
}

archive_cache_valid() {
archive_path="$1"
stat_path="$2"
themes_path="$3"
current_stat="$4"

[ -f "$stat_path" ] || return 1
[ -f "$themes_path" ] || return 1
[ "$(cat "$stat_path")" = "$current_stat" ] || return 1

while IFS= read -r theme_name; do
[ -n "$theme_name" ] || continue

if [ -f "$previews_root/$theme_name/config.json" ] &&
[ -f "$previews_root/$theme_name/source" ] &&
[ "$(cat "$previews_root/$theme_name/source")" = "$archive_path" ]; then
continue
fi

[ -f "$theme_dir/$theme_name/config.json" ] || return 1
done < "$themes_path"

return 0
}

archive_stat() {
# BusyBox stat supports this on Onion. If unavailable, the empty result
# simply keeps the existing MD5-based behavior.
stat -c '%s:%Y' "$1" 2>/dev/null
}

compare_hash() {
comp_dir="$1"
archive_hash="$2"

if [ -f "$comp_dir/md5hash" ]; then
comp_hash=$(cat "$comp_dir/md5hash")

if [ "$archive_hash" == "$comp_hash" ]; then
if [ "$archive_hash" = "$comp_hash" ]; then
return 0
fi
fi
Expand All @@ -112,7 +193,7 @@ compare_hash() {
}

md5hash() {
echo `md5sum "$1" | awk '{ print $1; }'`
md5sum "$1" | awk '{ print $1; }'
}

main
117 changes: 95 additions & 22 deletions src/themeSwitcher/themeSwitcher.c
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,94 @@

static bool quit = false;

#define PREVIEW_CACHE_SIZE 5

typedef struct {
int theme_index;
unsigned long last_used;
SDL_Surface *surface;
} PreviewCacheEntry;

static unsigned long preview_cache_clock = 0;

static bool resolvePreviewPath(const char *theme_name, char *path,
size_t path_size)
{
snprintf(path, path_size, THEMES_DIR "/%s/preview.png", theme_name);
if (is_file(path))
return true;

snprintf(path, path_size, THEMES_DIR "/.previews/%s/preview.png",
theme_name);
return is_file(path);
}

static SDL_Surface *loadPreviewSurface(const char *theme_name)
{
char path[STR_MAX * 2];
if (!resolvePreviewPath(theme_name, path, sizeof(path)))
return NULL;

SDL_Surface *preview = IMG_Load(path);
if (preview == NULL) {
printf_debug("Could not load theme preview: %s (%s)\n", path,
IMG_GetError());
}

// Keep the IMG_Load software surface. Converting it with
// SDL_DisplayFormat[Alpha]() flips previews on the Miyoo framebuffer.
return preview;
}

static void previewCacheInit(PreviewCacheEntry cache[PREVIEW_CACHE_SIZE])
{
for (int i = 0; i < PREVIEW_CACHE_SIZE; i++) {
cache[i].theme_index = -1;
cache[i].last_used = 0;
cache[i].surface = NULL;
}
}

static SDL_Surface *previewCacheGet(
PreviewCacheEntry cache[PREVIEW_CACHE_SIZE],
const char themes[NUMBER_OF_THEMES][STR_MAX], int theme_index)
{
preview_cache_clock++;

for (int i = 0; i < PREVIEW_CACHE_SIZE; i++) {
if (cache[i].theme_index == theme_index) {
cache[i].last_used = preview_cache_clock;
return cache[i].surface;
}
}

int slot = 0;
for (int i = 0; i < PREVIEW_CACHE_SIZE; i++) {
if (cache[i].theme_index < 0) {
slot = i;
break;
}
if (cache[i].last_used < cache[slot].last_used)
slot = i;
}

if (cache[slot].surface != NULL)
SDL_FreeSurface(cache[slot].surface);

cache[slot].theme_index = theme_index;
cache[slot].last_used = preview_cache_clock;
cache[slot].surface = loadPreviewSurface(themes[theme_index]);
return cache[slot].surface;
}

static void previewCacheFree(PreviewCacheEntry cache[PREVIEW_CACHE_SIZE])
{
for (int i = 0; i < PREVIEW_CACHE_SIZE; i++) {
if (cache[i].surface != NULL)
SDL_FreeSurface(cache[i].surface);
}
}

void showCenteredMessage(SDL_Surface *video, SDL_Surface *screen,
const char *message_str, TTF_Font *font,
SDL_Color color)
Expand Down Expand Up @@ -197,25 +285,10 @@ int main(int argc, char *argv[])
int themes_count = listAllThemes(themes, installed_theme, &installed_page);
int current_page = installed_page;

showCenteredMessage(video, screen, "Loading previews...", font30, color_white);

char preview_path[STR_MAX * 2];
SDL_Surface *previews[themes_count];
PreviewCacheEntry preview_cache[PREVIEW_CACHE_SIZE];
previewCacheInit(preview_cache);
SDL_Surface *noPreview = IMG_Load("res/noThemePreview.png");

for (int i = 0; i < themes_count; i++) {
snprintf(preview_path, STR_MAX * 2 - 1, THEMES_DIR "/%s/preview.png", themes[i]);

if (!is_file(preview_path))
snprintf(preview_path, STR_MAX * 2 - 1, THEMES_DIR "/.previews/%s/preview.png", themes[i]);

previews[i] = is_file(preview_path) ? IMG_Load(preview_path) : NULL;

char loading_msg[STR_MAX];
snprintf(loading_msg, STR_MAX - 1, "Loading previews... %d/%d", i + 1, themes_count);
showCenteredMessage(video, screen, loading_msg, font30, color_white);
}

char cPages[25];

SDL_Event event;
Expand Down Expand Up @@ -326,11 +399,13 @@ int main(int argc, char *argv[])
continue;

if (levelPage == 0) {
if (previews[current_page] == NULL) {
SDL_Surface *preview = previewCacheGet(
preview_cache, themes, current_page);
if (preview == NULL) {
SDL_BlitSurface(noPreview, NULL, screen, &rectThemePreview);
}
else {
SDL_BlitSurface(previews[current_page], &preview_src_rect, screen, &rectThemePreview);
SDL_BlitSurface(preview, &preview_src_rect, screen, &rectThemePreview);
}
SDL_BlitSurface(background_page0, NULL, screen, NULL);

Expand Down Expand Up @@ -407,9 +482,7 @@ int main(int argc, char *argv[])

msleep(100);

for (int i = 0; i < themes_count; i++) {
SDL_FreeSurface(previews[i]);
}
previewCacheFree(preview_cache);
SDL_FreeSurface(noPreview);
SDL_FreeSurface(surfaceArrowLeft);
SDL_FreeSurface(surfaceArrowRight);
Expand Down