Initial Commit
This commit is contained in:
22
.gitignore
vendored
Normal file
22
.gitignore
vendored
Normal 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
2
dscanner.ini
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[analysis.config.ModuleFilters]
|
||||||
|
style_check="-medimancer.app"
|
||||||
26
dub.json
Normal file
26
dub.json
Normal 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
19
dub.selections.json
Normal 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
47
prebuild.d
Normal 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
120
source/medimancer/app.d
Normal 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();
|
||||||
|
}
|
||||||
11
source/medimancer/exception.d
Normal file
11
source/medimancer/exception.d
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
78
source/medimancer/ffmpeg.d
Normal file
78
source/medimancer/ffmpeg.d
Normal 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
115
source/medimancer/popup.d
Normal 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;
|
||||||
|
}
|
||||||
77
source/medimancer/registry.d
Normal file
77
source/medimancer/registry.d
Normal 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
14
views/res/i18n/en.ini
Normal 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
14
views/res/i18n/lt.ini
Normal 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
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
1
views/res/res.rc
Normal file
@ -0,0 +1 @@
|
|||||||
|
1 ICON "img/icon.ico"
|
||||||
Reference in New Issue
Block a user