Initial Commit

This commit is contained in:
2025-04-27 07:25:09 +02:00
commit 1b8df26098
14 changed files with 546 additions and 0 deletions

22
.gitignore vendored Normal file
View File

@ -0,0 +1,22 @@
.dub
docs.json
__dummy.html
docs/
/medimancer
medimancer.so
medimancer.dylib
medimancer.dll
medimancer.a
medimancer.lib
medimancer-test-*
*.exe
*.pdb
*.o
*.obj
*.lst
*.log
# Compiled resources
/views/resources.list
/views/res/img/icon.ico
medimancer.res

2
dscanner.ini Normal file
View File

@ -0,0 +1,2 @@
[analysis.config.ModuleFilters]
style_check="-medimancer.app"

26
dub.json Normal file
View File

@ -0,0 +1,26 @@
{
"authors": [
"Tomas"
],
"copyright": "Copyright © 2025, Tomas",
"dependencies": {
"dlangui": "~>0.10.8"
},
"description": "A shell extension for windows, to allow for easy media file conversions, using ffmpeg.",
"license": "MIT",
"name": "medimancer",
"preBuildCommands-windows": [
"rdmd prebuild.d"
],
"sourceFiles-windows": [
"medimancer.res"
],
"subConfigurations": {
"dlangui": "minimal"
},
"stringImportPaths": [
"views",
"views/res",
"views/res/i18n"
]
}

19
dub.selections.json Normal file
View File

@ -0,0 +1,19 @@
{
"fileVersion": 1,
"versions": {
"arsd-official": "10.9.10",
"bindbc-common": "0.1.6",
"bindbc-freetype": "1.2.6",
"bindbc-loader": "1.1.5",
"bindbc-opengl": "1.1.1",
"bindbc-sdl": "1.4.8",
"dlangui": "0.10.8",
"dsfml": "2.1.1",
"glx-d": "1.1.0",
"icontheme": "1.2.3",
"inilike": "1.2.2",
"isfreedesktop": "0.1.1",
"x11": "1.0.21",
"xdgpaths": "0.2.5"
}
}

47
prebuild.d Normal file
View File

@ -0,0 +1,47 @@
module prebuild;
import std;
void exec(string[] args)
{
auto res = execute(args);
if (res.status == 0)
return;
writeln(res.output);
throw new Exception("Command failed");
}
void main()
{
// Generate icon file
exec([
"magick",
"views/res/img/icon.png",
"-alpha", "on",
"-background", "transparent",
"(", "-clone", "0", "-scale", "16x16", "-extent", "16x16", ")",
"(", "-clone", "0", "-scale", "32x32", "-extent", "32x32", ")",
"(", "-clone", "0", "-scale", "48x48", "-extent", "48x48", ")",
"(", "-clone", "0", "-scale", "64x64", "-extent", "64x64", ")",
"(", "-clone", "0", "-scale", "96x96", "-extent", "96x96", ")",
"(", "-clone", "0", "-scale", "128x128", "-extent", "128x128", ")",
"(", "-clone", "0", "-scale", "256x256", "-extent", "256x256", ")",
"-delete", "0",
"-colors", "256",
"views/res/img/icon.ico",
]);
// Generate resource file
exec(["rc", "/fo", "medimancer.res", "views\\res\\res.rc"]);
// Generate resources.list
string view = absolutePath("views\\");
File resources = File(buildPath(view, "resources.list"), "w");
foreach (entry; dirEntries(buildPath(view, "res"), SpanMode.breadth))
{
if (!entry.isFile)
continue;
resources.writeln(asRelativePath(entry, view).array.replace('\\', '/'));
}
}

120
source/medimancer/app.d Normal file
View File

@ -0,0 +1,120 @@
module medimancer.app;
import dlangui;
mixin APP_ENTRY_POINT;
import std.file;
import std.path;
import std.format;
import medimancer.popup;
import medimancer.ffmpeg;
import medimancer.registry;
import medimancer.exception;
struct Action
{
string file_name;
string format;
}
private void configure()
{
void install()
{
auto basicKey = new Key(r"SOFTWARE\Classes\*\shell\medimancer");
auto basicCommandKey = new Key(r"SOFTWARE\Classes\*\shell\medimancer\command");
basicKey.set(null, "MediMancer");
basicKey.set("icon", format(`"%s"`, thisExePath));
basicCommandKey.set(null, format(`"%s" "%%V"`, thisExePath));
auto mp3Key = new Key(r"SOFTWARE\Classes\*\shell\medimancer_mp3");
auto mp3CommandKey = new Key(r"SOFTWARE\Classes\*\shell\medimancer_mp3\command");
mp3Key.set(null, "MediMancer MP3");
mp3Key.set("icon", format(`"%s"`, thisExePath));
mp3CommandKey.set(null, format(`"%s" -fmt mp3 "%%V"`, thisExePath));
auto mp4Key = new Key(r"SOFTWARE\Classes\*\shell\medimancer_mp4");
auto mp4CommandKey = new Key(r"SOFTWARE\Classes\*\shell\medimancer_mp4\command");
mp4Key.set(null, "MediMancer MP4");
mp4Key.set("icon", format(`"%s"`, thisExePath));
mp4CommandKey.set(null, format(`"%s" -fmt mp4 "%%V"`, thisExePath));
}
void uninstall()
{
auto shellKey = new Key(r"SOFTWARE\Classes\*\shell");
shellKey.remove("medimancer");
shellKey.remove("medimancer_mp3");
shellKey.remove("medimancer_mp4");
}
class InstallPopup : Popup
{
public:
this()
{
super("install", 160, 72);
auto ibut = new Button(null, "install");
auto ubut = new Button(null, "uninstall");
ibut.click = (widget){install(); window.close(); return true;};
ubut.click = (widget){uninstall(); window.close(); return true;};
window.mainWidget = new VerticalLayout()
.addChildren([ibut, ubut]);
show();
}
}
new InstallPopup();
}
private void process(Action action)
{
if (!action.format)
new InputPopup("select_format", (format){
convertMedia(action.file_name, setExtension(action.file_name, format));
});
else
convertMedia(action.file_name, setExtension(action.file_name, action.format));
}
extern (C) int UIAppMain(string[] args)
{
embeddedResourceList.addResources(embedResourcesFromList!("resources.list")());
try
{
if (args.length == 1)
configure();
else
{
Action action;
for (size_t i = 1; i < args.length; i++)
{
switch (args[i])
{
case "-fmt":
action.format = args[++i];
break;
default:
action.file_name = args[i];
break;
}
}
process(action);
}
}
catch (UserError ex)
{
new ErrorPopup(ex.message.idup);
}
catch (Exception ex)
{
new ErrorPopup(ex.toString, true);
}
return Platform.instance.enterMessageLoop();
}

View File

@ -0,0 +1,11 @@
module medimancer.exception;
class UserError : Exception
{
this(string msg, string file = __FILE__, size_t line = __LINE__, Throwable nextInChain = null)
pure nothrow @nogc @safe
{
super(msg, file, line, nextInChain);
}
}

View File

@ -0,0 +1,78 @@
module medimancer.ffmpeg;
import core.time;
import std.parallelism;
import std.exception;
import std.algorithm;
import std.process;
import std.string;
import std.format;
import std.array;
import std.file;
import std.path;
import std.conv;
import medimancer.exception;
import medimancer.popup;
void convertMedia(string from, string to)
{
enforce!UserError(exists(from), "file_does_not_exist");
enforce!UserError(from != to, "destination_is_source");
auto probe = execute(["ffprobe", from]);
enforce!UserError(!probe.status, "probe_failed");
Duration duration;
int filled;
foreach (line; probe.output.splitLines)
{
if (!line.startsWith(" Duration:"))
continue;
int hrs, min, sec, msc;
filled = formattedRead(line.strip(), "Duration: %d:%d:%d.%d", hrs, min, sec, msc);
duration = hours(hrs) + minutes(min) + seconds(sec) + msecs(msc);
break;
}
enforce!UserError("probe_invalid");
uint progress_end = cast(uint)duration.total!"msecs";
uint progress = 0;
ProcessPipes pipes = pipeProcess(
["ffmpeg", "-i", from, to],
Redirect.all,
null,
Config.suppressConsole,
null
);
pipes.stdin.writeln("yes");
task((){
foreach (line; pipes.stderr.byLineCopy(KeepTerminator.no, '\r'))
{
line = line.strip;
if (!line.startsWith("frame="))
continue;
int hrs, min, sec, msc;
try
line.find("time=").formattedRead("time=%d:%d:%d.%d", hrs, min, sec, msc);
catch (Exception ex)
continue;
Duration curr = hours(hrs) + minutes(min) + seconds(sec) + msecs(msc);
uint progress_curr = cast(uint)(curr.total!"msecs" * 1000);
progress = progress_curr / progress_end;
}
progress = 1000;
}).executeInNewThread;
ProgressPopup popup;
popup = new ProgressPopup("converting", {
if (progress == 1000)
popup.finish();
return progress;
});
}

115
source/medimancer/popup.d Normal file
View File

@ -0,0 +1,115 @@
module medimancer.popup;
import dlangui;
import std.traits;
alias UI = UIString.fromId;
private UIString RUI(S)(S str)
if (isSomeString!S)
{
return UIString.fromRaw(str.to!dstring);
}
class Popup
{
public:
void show()
{
window.show();
}
protected:
this(string title, int width, int height)
{
window = Platform.instance.createWindow(UI(title), null, 0, width, height);
}
Window window;
}
class ErrorPopup : Popup
{
public:
this(string message, bool raw = false)
{
super("error", 240, 64);
if (raw)
window.mainWidget = new TextWidget(null, RUI(message));
else
window.mainWidget = new TextWidget(null, UI(message));
show();
}
}
class InputPopup : Popup
{
public:
this(string label, void delegate(string input) callback)
{
super(label, 240, 96);
auto input = new EditLine();
auto button = new Button(null, "enter");
auto box = new GroupBox(null, label);
box.addChild(input);
box.addChild(button);
window.mainWidget = box;
button.click = delegate(Widget src)
{
callback(input.text.to!string);
window.close();
return true;
};
show();
}
}
class ProgressPopup : Popup
{
public:
this(string label, uint delegate() callback)
{
super(label, 512, 64);
window.mainWidget = new Progress("progress", callback);
timer = window.mainWidget.setTimer(Progress.updateRate);
show();
}
void finish()
{
window.mainWidget.cancelTimer(timer);
window.close();
}
protected:
class Progress : VerticalLayout
{
this(string label, uint delegate() callback)
{
this.callback = callback;
progressBar = new ProgressBarWidget();
progressBar.animationInterval(updateRate);
addChild(new TextWidget(null, UI(label)));
addChild(progressBar);
}
override bool onTimer(ulong id)
{
progressBar.progress = callback();
return true;
}
enum updateRate = 1000/24;
uint delegate() callback;
ProgressBarWidget progressBar;
}
ulong timer;
}

View File

@ -0,0 +1,77 @@
module medimancer.registry;
import core.sys.windows.windows;
import std.conv;
import std.string;
import std.exception;
extern (Windows) LONG RegDeleteTreeA(HKEY hKey, LPCSTR lpSubKey);
class Key
{
public:
this(HKEY hKey, string subKey) @trusted
{
LONG result = RegCreateKeyExA(
hKey,
subKey.toStringz,
0,
null,
0,
KEY_ALL_ACCESS,
null,
&handle,
null
);
enforce(result == ERROR_SUCCESS, "Failed to open registry key \""~subKey~"\"");
}
this(string key)
{
this(HKEY_CURRENT_USER, key);
}
~this()
{
RegCloseKey(handle);
}
void set(string value, uint type, scope const(void)[] data)
{
LONG status = RegSetValueExA(
handle,
value ? value.toStringz : null,
0,
type,
cast(const ubyte*)data.ptr,
(cast(ubyte[])data).length.to!uint,
);
enforce(status == ERROR_SUCCESS, "Failed to set data \""~value~"\"");
}
void set(string value, uint dword)
{
set(value, REG_DWORD, (&dword)[0..1]);
}
void set(string value, string str)
{
set(value, REG_SZ, str.toStringz[0..str.length]);
}
void remove(string subKey)
{
LONG status = RegDeleteTreeA(
handle,
subKey.toStringz
);
enforce(status == ERROR_SUCCESS, "Failed to remove key \""~subKey~"\"");
}
protected:
HKEY handle;
}

14
views/res/i18n/en.ini Normal file
View File

@ -0,0 +1,14 @@
install = Install
uninstall = Uninstall
error = Error
enter = Enter
progress = Progress
select_format = Select format
file_does_not_exist = File does not exist
destination_is_source = Destination file is same as source
probe_failed = Probe failed
probe_invalid = Probe returned invalid data
converting = Converting...

14
views/res/i18n/lt.ini Normal file
View File

@ -0,0 +1,14 @@
install = Įrašyti
uninstall = Ištrinti
error = Klaida
enter = Toliau
progress = Pažanga
select_format = Pasirinkite formatą
file_does_not_exist = Įrašas neegzistuoja
destination_is_source = Galutinis įrašas lygus duotam
probe_failed = Patikra nepavyko
probe_invalid = Patikra atsakė klaidingai
converting = Perašoma...

BIN
views/res/img/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 589 B

1
views/res/res.rc Normal file
View File

@ -0,0 +1 @@
1 ICON "img/icon.ico"