7 Commits
9 changed files with 302 additions and 15 deletions
@@ -0,0 +1,29 @@
name: Build (nix)
on:
workflow_run:
workflows:
- Update flake lock
types:
- completed
permissions:
contents: read
jobs:
build:
if: ${{ gitea.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
# The update workflow may have pushed a newer flake.lock commit.
ref: ${{ gitea.event.workflow_run.head_branch }}
- name: Install upstream Nix
uses: cachix/install-nix-action@v31
- name: Build flake
run: nix build
+11
View File
@@ -0,0 +1,11 @@
[Desktop Entry]
Type=Application
Name=SNES Patcher
GenericName=ROM Patcher
Comment=Apply IPS and BPS patches to Super Nintendo ROMs
Exec=snespatcher
Icon=com.wyattjmiller.snespatcher
Terminal=false
Categories=Utility;
Keywords=SNES;ROM;IPS;BPS;Patch;
StartupNotify=true
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>com.wyattjmiller.snespatcher</id>
<metadata_license>CC0-1.0</metadata_license>
<project_license>MPL-2.0</project_license>
<name>SNES Patcher</name>
<summary>Apply IPS and BPS patches to Super Nintendo ROMs</summary>
<description>
<p>
SNES Patcher creates patched copies of Super Nintendo ROMs
while preserving the original file.
</p>
</description>
<launchable type="desktop-id">
com.wyattjmiller.snespatcher.desktop
</launchable>
<url type="homepage">
https://scm.wyattjmiller.com/wymiller/snespatcher
</url>
<content_rating type="oars-1.1"/>
</component>
@@ -0,0 +1,102 @@
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256">
<defs>
<linearGradient id="body" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#565b66"/>
<stop offset="1" stop-color="#353944"/>
</linearGradient>
<linearGradient id="panel" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#4b505b"/>
<stop offset="1" stop-color="#373b45"/>
</linearGradient>
<linearGradient id="gold" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#ffd36a"/>
<stop offset="1" stop-color="#e9a834"/>
</linearGradient>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="150%">
<feDropShadow dx="0" dy="5" stdDeviation="5" flood-color="#000" flood-opacity=".28"/>
</filter>
<filter id="arrowShadow" x="-30%" y="-30%" width="160%" height="160%">
<feDropShadow dx="0" dy="2" stdDeviation="2" flood-color="#000" flood-opacity=".35"/>
</filter>
</defs>
<!-- Cartridge -->
<g filter="url(#shadow)">
<!-- main shell -->
<path d="
M55 26
Q44 26 39 33
Q35 39 35 49
L35 72
Q35 79 29 84
L24 88
L24 100
L30 104
Q35 108 35 116
L35 190
Q35 203 44 212
Q51 220 63 220
L193 220
Q205 220 212 212
Q221 203 221 190
L221 116
Q221 108 226 104
L232 100
L232 88
L227 84
Q221 79 221 72
L221 49
Q221 39 216 33
Q211 26 200 26
Z"
fill="url(#body)" stroke="#2b2e36" stroke-width="5" stroke-linejoin="round"/>
<!-- top grooves -->
<rect x="61" y="49" width="134" height="7" rx="3.5" fill="#292d35"/>
<rect x="61" y="63" width="134" height="7" rx="3.5" fill="#292d35"/>
<!-- center inset -->
<rect x="64" y="88" width="128" height="101" rx="15"
fill="#30343d" stroke="#252932" stroke-width="5"/>
<rect x="70" y="94" width="116" height="89" rx="11"
fill="url(#panel)" stroke="#626874" stroke-width="2"/>
<!-- tiny triangle notch -->
<path d="M118 194 L138 194 L128 205 Z" fill="#252932"/>
<!-- gold contacts -->
<rect x="61" y="207" width="134" height="23" rx="4"
fill="url(#gold)" stroke="#7e581c" stroke-width="3"/>
<g stroke="#b77920" stroke-width="3">
<line x1="78" y1="208" x2="78" y2="229"/>
<line x1="95" y1="208" x2="95" y2="229"/>
<line x1="112" y1="208" x2="112" y2="229"/>
<line x1="129" y1="208" x2="129" y2="229"/>
<line x1="146" y1="208" x2="146" y2="229"/>
<line x1="163" y1="208" x2="163" y2="229"/>
<line x1="180" y1="208" x2="180" y2="229"/>
</g>
</g>
<!-- down arrow -->
<path d="
M119 112
Q119 106 125 106
L131 106
Q137 106 137 112
L137 144
L151 130
Q155 126 159 130
L164 135
Q168 139 164 143
L132 175
Q128 179 124 175
L92 143
Q88 139 92 135
L97 130
Q101 126 105 130
L119 144
Z"
fill="#f2f2f2" stroke="#d7d7d7" stroke-width="2" stroke-linejoin="round"
filter="url(#arrowShadow)"/>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

Generated
+3 -3
View File
@@ -2,11 +2,11 @@
"nodes": { "nodes": {
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1787498568, "lastModified": 1788614874,
"narHash": "sha256-9i/VTdusq/+NM/tz+J1Re+ojkMB8MBf0QshnYfzHz30=", "narHash": "sha256-7QYjT2vHLuX9Z1pdxHXDKCbh1CR3D/2rywB9Tx0MPRg=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "56c02bc00adcf003215cc4bd996d6efaf4cff188", "rev": "c043004d1c6985732bcc1cbc5a9c9aecbbb4e0f0",
"type": "github" "type": "github"
}, },
"original": { "original": {
+36 -6
View File
@@ -3,7 +3,8 @@
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
outputs = { self, nixpkgs }: outputs =
{ self, nixpkgs }:
let let
supportedSystems = [ supportedSystems = [
"x86_64-linux" "x86_64-linux"
@@ -12,7 +13,16 @@
forAllSystems = nixpkgs.lib.genAttrs supportedSystems; forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
in in
{ {
packages = forAllSystems (system: formatter = forAllSystems (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
in
pkgs.nixfmt-rfc-style
);
packages = forAllSystems (
system:
let let
pkgs = nixpkgs.legacyPackages.${system}; pkgs = nixpkgs.legacyPackages.${system};
in in
@@ -23,7 +33,8 @@
src = pkgs.lib.cleanSourceWith { src = pkgs.lib.cleanSourceWith {
src = ./.; src = ./.;
filter = path: type: filter =
path: type:
let let
name = baseNameOf path; name = baseNameOf path;
in in
@@ -47,6 +58,20 @@
gtk4 gtk4
]; ];
postInstall = ''
install -Dm444 \
data/com.wyattjmiller.snespatcher.desktop \
$out/share/applications/com.wyattjmiller.snespatcher.desktop
install -Dm444 \
data/com.wyattjmiller.snespatcher.metainfo.xml \
$out/share/metainfo/com.wyattjmiller.snespatcher.metainfo.xml
install -Dm444 \
data/icons/hicolor/scalable/apps/com.wyattjmiller.snespatcher.svg \
$out/share/icons/hicolor/scalable/apps/com.wyattjmiller.snespatcher.svg
'';
meta = { meta = {
description = "GTK 4 application for applying IPS patches to SNES ROMs"; description = "GTK 4 application for applying IPS patches to SNES ROMs";
homepage = "https://scm.wyattjmiller.com/wymiller/snespatcher"; homepage = "https://scm.wyattjmiller.com/wymiller/snespatcher";
@@ -57,9 +82,11 @@
}; };
default = snespatcher; default = snespatcher;
}); }
);
devShells = forAllSystems (system: devShells = forAllSystems (
system:
let let
pkgs = nixpkgs.legacyPackages.${system}; pkgs = nixpkgs.legacyPackages.${system};
in in
@@ -72,8 +99,11 @@
clippy clippy
rustc rustc
rustfmt rustfmt
desktop-file-utils
appstream
]; ];
}; };
}); }
);
}; };
} }
+17
View File
@@ -2,6 +2,23 @@
<interface> <interface>
<requires lib="gtk" version="4.0"/> <requires lib="gtk" version="4.0"/>
<menu id="app-menu"> <menu id="app-menu">
<section>
<submenu>
<attribute name="label" translatable="yes">_Appearance</attribute>
<section>
<item>
<attribute name="label" translatable="yes">Follow _System</attribute>
<attribute name="action">app.theme</attribute>
<attribute name="target">system</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Dark</attribute>
<attribute name="action">app.theme</attribute>
<attribute name="target">dark</attribute>
</item>
</section>
</submenu>
</section>
<section> <section>
<item> <item>
<attribute name="label" translatable="yes">_About SNES Patcher</attribute> <attribute name="label" translatable="yes">_About SNES Patcher</attribute>
+12 -5
View File
@@ -2,11 +2,6 @@
min-width: 356px; min-width: 356px;
} }
.title {
font-size: 1.5rem;
font-weight: 700;
}
.heading { .heading {
font-weight: 600; font-weight: 600;
} }
@@ -21,3 +16,15 @@
min-height: 48px; min-height: 48px;
padding: 10px 12px; padding: 10px 12px;
} }
.intro-title {
font-size: 1.5rem;
font-weight: 700;
}
headerbar.titlebar {
background-color: transparent;
background-image: none;
border-bottom: none;
box-shadow: none;
}
+65 -1
View File
@@ -7,6 +7,7 @@ use std::rc::Rc;
use gtk4::gio; use gtk4::gio;
use gtk4::glib; use gtk4::glib;
use gtk4::glib::ToVariant;
use gtk4::pango::EllipsizeMode; use gtk4::pango::EllipsizeMode;
use gtk4::prelude::*; use gtk4::prelude::*;
use gtk4::{ use gtk4::{
@@ -16,6 +17,9 @@ use gtk4::{
}; };
const APP_ID: &str = "com.wyattjmiller.snespatcher"; const APP_ID: &str = "com.wyattjmiller.snespatcher";
const APP_NAME: &str = "SNES Patcher";
const THEME_SYSTEM: &str = "system";
const THEME_DARK: &str = "dark";
#[derive(Clone)] #[derive(Clone)]
struct SelectionState { struct SelectionState {
@@ -31,6 +35,7 @@ struct SelectionRow {
} }
fn main() { fn main() {
glib::set_application_name(APP_NAME);
let app = Application::new(Some(APP_ID), Default::default()); let app = Application::new(Some(APP_ID), Default::default());
app.connect_activate(build_ui); app.connect_activate(build_ui);
app.run(); app.run();
@@ -69,7 +74,7 @@ fn build_ui(app: &Application) {
let introduction = GtkBox::new(Orientation::Vertical, 8); let introduction = GtkBox::new(Orientation::Vertical, 8);
let heading = Label::new(Some("Patch a Super Nintendo ROM")); let heading = Label::new(Some("Patch a Super Nintendo ROM"));
heading.add_css_class("title"); heading.add_css_class("intro-title");
heading.set_justify(Justification::Center); heading.set_justify(Justification::Center);
heading.set_wrap(true); heading.set_wrap(true);
@@ -190,10 +195,14 @@ fn build_header_bar(
header_bar.pack_start(rom_info_button); header_bar.pack_start(rom_info_button);
header_bar.pack_end(&menu_button); header_bar.pack_end(&menu_button);
add_theme_action(app);
let about = gio::SimpleAction::new("about", None); let about = gio::SimpleAction::new("about", None);
about.connect_activate(glib::clone!(@weak window => move |_, _| { about.connect_activate(glib::clone!(@weak window => move |_, _| {
let dialog = AboutDialog::builder() let dialog = AboutDialog::builder()
.transient_for(&window) .transient_for(&window)
.program_name(APP_NAME)
.logo_icon_name(APP_ID)
.modal(true) .modal(true)
.program_name("SNES Patcher") .program_name("SNES Patcher")
.version(env!("CARGO_PKG_VERSION")) .version(env!("CARGO_PKG_VERSION"))
@@ -215,6 +224,61 @@ fn build_header_bar(
header_bar header_bar
} }
fn add_theme_action(app: &Application) {
let preference = load_theme_preference();
let settings = gtk4::Settings::default().expect("Could not access GTK settings");
apply_theme_preference(&settings, &preference);
let theme = gio::SimpleAction::new_stateful(
"theme",
Some(glib::VariantTy::STRING),
&preference.to_variant(),
);
theme.connect_activate(move |action, parameter| {
let Some(preference) = parameter.and_then(|value| value.get::<String>()) else {
return;
};
if preference != THEME_SYSTEM && preference != THEME_DARK {
return;
}
apply_theme_preference(&settings, &preference);
action.set_state(&preference.to_variant());
save_theme_preference(&preference);
});
app.add_action(&theme);
}
fn apply_theme_preference(settings: &gtk4::Settings, preference: &str) {
settings.set_gtk_application_prefer_dark_theme(preference == THEME_DARK);
}
fn theme_preference_path() -> PathBuf {
glib::user_config_dir().join(APP_ID).join("theme")
}
fn load_theme_preference() -> String {
match std::fs::read_to_string(theme_preference_path()) {
Ok(preference) if preference.trim() == THEME_DARK => THEME_DARK.to_string(),
_ => THEME_SYSTEM.to_string(),
}
}
fn save_theme_preference(preference: &str) {
let path = theme_preference_path();
let Some(parent) = path.parent() else {
return;
};
if let Err(error) = std::fs::create_dir_all(parent) {
eprintln!("Could not create the preferences directory: {error}");
return;
}
if let Err(error) = std::fs::write(path, preference) {
eprintln!("Could not save the theme preference: {error}");
}
}
fn selection_row(title: &str, placeholder: &str, icon_name: &str) -> SelectionRow { fn selection_row(title: &str, placeholder: &str, icon_name: &str) -> SelectionRow {
let icon = Image::from_icon_name(icon_name); let icon = Image::from_icon_name(icon_name);
icon.set_pixel_size(24); icon.set_pixel_size(24);