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