feat: external player support

close #388
This commit is contained in:
edde746
2026-02-11 14:59:52 +01:00
parent 45e72e0ad9
commit 03907db6c4
36 changed files with 1465 additions and 2 deletions
+82
View File
@@ -0,0 +1,82 @@
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="display:inline;enable-background:new" version="1.0" id="svg11300" viewBox="8 28 112 88">
<title id="title4162">Adwaita Icon Template</title>
<defs id="defs3">
<linearGradient id="linearGradient1965">
<stop style="stop-color:#62a0ea;stop-opacity:1" offset="0" id="stop1961"/>
<stop style="stop-color:#1a5fb4;stop-opacity:1" offset="1" id="stop1963"/>
</linearGradient>
<linearGradient id="linearGradient1959">
<stop style="stop-color:#ed333b;stop-opacity:1" offset="0" id="stop1955"/>
<stop style="stop-color:#1a5fb4;stop-opacity:0" offset="1" id="stop1957"/>
</linearGradient>
<radialGradient r="225" fy="-1136.1257" fx="58.953751" cy="-1136.1257" cx="58.953751" gradientTransform="matrix(0.27963724,0,0,0.19167088,14.08685,422.59065)" gradientUnits="userSpaceOnUse" id="radialGradient2319" xlink:href="#linearGradient1965"/>
<radialGradient r="225" fy="-173.6787" fx="312.7467" cy="-173.6787" cx="312.7467" gradientTransform="matrix(0.60444443,0,0,0.35619377,-109.038,345.86327)" gradientUnits="userSpaceOnUse" id="radialGradient2282" xlink:href="#linearGradient1959"/>
</defs>
<metadata id="metadata4">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:creator>
<cc:Agent>
<dc:title>GNOME Design Team</dc:title>
</cc:Agent>
</dc:creator>
<dc:source/>
<cc:license rdf:resource="http://creativecommons.org/licenses/by-sa/4.0/"/>
<dc:title>Adwaita Icon Template</dc:title>
<dc:subject>
<rdf:Bag/>
</dc:subject>
<dc:date/>
<dc:rights>
<cc:Agent>
<dc:title/>
</cc:Agent>
</dc:rights>
<dc:publisher>
<cc:Agent>
<dc:title/>
</cc:Agent>
</dc:publisher>
<dc:identifier/>
<dc:relation/>
<dc:language/>
<dc:coverage/>
<dc:description/>
<dc:contributor>
<cc:Agent>
<dc:title/>
</cc:Agent>
</dc:contributor>
</cc:Work>
<cc:License rdf:about="http://creativecommons.org/licenses/by-sa/4.0/">
<cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction"/>
<cc:permits rdf:resource="http://creativecommons.org/ns#Distribution"/>
<cc:requires rdf:resource="http://creativecommons.org/ns#Notice"/>
<cc:requires rdf:resource="http://creativecommons.org/ns#Attribution"/>
<cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks"/>
<cc:requires rdf:resource="http://creativecommons.org/ns#ShareAlike"/>
</cc:License>
</rdf:RDF>
</metadata>
<g transform="translate(0,-172)" style="display:inline" id="layer1">
<g style="display:inline" id="layer9">
<path id="rect1792-1-3" d="m 8,200 v 88 h 8 v -8 h 8 v 8 h 80 v -8 h 8 v 8 h 8 v -88 h -8 v 8 h -8 v -8 H 24 v 8 h -8 v -8 z m 8,16 h 8 v 8 h -8 z m 88,0 h 8 v 8 h -8 z m -88,16 h 8 v 8 h -8 z m 88,0 h 8 v 8 h -8 z m -88,16 h 8 v 8 h -8 z m 88,0 h 8 v 8 h -8 z m -88,16 h 8 v 8 h -8 z m 88,0 h 8 v 8 h -8 z" style="display:inline;opacity:1;vector-effect:none;fill:url(#radialGradient2319);fill-opacity:1;stroke:none;stroke-width:0.33206606;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:0.33206605, 0.66413209;stroke-dashoffset:0;stroke-opacity:0.94881891;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new"/>
<path id="rect1794-0-6" d="m 8,200 v 88 h 8 v -8 h 8 v 8 h 80 v -8 h 8 v 8 h 8 v -88 h -8 v 8 h -8 v -8 H 24 v 8 h -8 v -8 z m 8,16 h 8 v 8 h -8 z m 88,0 h 8 v 8 h -8 z m -88,16 h 8 v 8 h -8 z m 88,0 h 8 v 8 h -8 z m -88,16 h 8 v 8 h -8 z m 88,0 h 8 v 8 h -8 z m -88,16 h 8 v 8 h -8 z m 88,0 h 8 v 8 h -8 z" style="display:inline;opacity:1;vector-effect:none;fill:url(#radialGradient2282);fill-opacity:1;stroke:none;stroke-width:0.33206606;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:0.33206605, 0.66413209;stroke-dashoffset:0;stroke-opacity:0.94881891;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new"/>
<path id="path2574-9" d="m 63.999999,218 a 20,20 0 0 0 -20,20 20,20 0 0 0 20,20 20,20 0 0 0 20,-20 20,20 0 0 0 -20,-20 z m -7.90625,10 h 1.8125 0.1875 c 0.34982,-0.003 0.69558,0.0774 1,0.25 l 14,8 c 0.6214,0.34377 0.9375,1.0469 0.9375,1.75 0,0.7031 -0.3161,1.40622 -0.9375,1.75 l -14,8 c -0.30442,0.1724 -0.65018,0.252 -1,0.25 h -0.1875 -1.8125 z" style="display:inline;opacity:0.15;vector-effect:none;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.33206606;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:0.33206605, 0.66413209;stroke-dashoffset:0;stroke-opacity:0.94881891;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new"/>
<g transform="translate(-9e-7,8)" style="display:inline;fill:#ffffff;enable-background:new" id="g2594">
<path style="opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.33206606;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:0.33206605, 0.66413209;stroke-dashoffset:0;stroke-opacity:0.94881891;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal" d="m 64,208 a 20,20 0 0 0 -20,20 20,20 0 0 0 20,20 20,20 0 0 0 20,-20 20,20 0 0 0 -20,-20 z m -7.90625,10 h 1.8125 0.1875 c 0.34982,-0.003 0.69558,0.0774 1,0.25 l 14,8 c 0.6214,0.34377 0.9375,1.0469 0.9375,1.75 0,0.7031 -0.3161,1.40622 -0.9375,1.75 l -14,8 c -0.30442,0.1724 -0.65018,0.252 -1,0.25 h -0.1875 -1.8125 z" id="path2574"/>
</g>
<g transform="translate(-9e-7,10)" style="display:inline;opacity:0.15;fill:#000000;enable-background:new" id="g2572">
<rect style="display:inline;opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.33206606;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:0.33206605, 0.66413209;stroke-dashoffset:0;stroke-opacity:0.94881891;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new" id="rect2327-2" width="64" height="4" x="32" y="260" rx="2" ry="2"/>
<rect style="display:inline;opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.24904953;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:0.24904954, 0.49809907;stroke-dashoffset:0;stroke-opacity:0.94881891;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new" id="rect2327-4-6" width="12" height="12" x="40" y="256" rx="6" ry="6"/>
</g>
<g transform="translate(-9e-7,8)" id="g2719" style="display:inline;enable-background:new">
<rect style="opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.33206606;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:0.33206605, 0.66413209;stroke-dashoffset:0;stroke-opacity:0.94881891;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal" id="rect2327" width="64" height="4" x="32" y="260" rx="2" ry="2"/>
<rect style="display:inline;opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.24904953;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:0.24904954, 0.49809907;stroke-dashoffset:0;stroke-opacity:0.94881891;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new" id="rect2327-4" width="12" height="12" x="40" y="256" rx="6" ry="6"/>
</g>
<path id="path1927" d="m 16,213 v 3 h 8 v -3 z m 88,0 v 3 h 8 v -3 z m -88,16 v 3 h 8 v -3 z m 88,0 v 3 h 8 v -3 z m -88,16 v 3 h 8 v -3 z m 88,0 v 3 h 8 v -3 z m -88,16 v 3 h 8 v -3 z m 88,0 v 3 h 8 v -3 z m -88,16 v 3 h 8 v -3 z m 88,0 v 3 h 8 v -3 z m -96,8 v 3 h 8 v -3 z m 16,0 v 3 h 80 v -3 z m 88,0 v 3 h 8 v -3 z" style="display:inline;opacity:0.25;vector-effect:none;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.33206606;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:0.33206605, 0.66413209;stroke-dashoffset:0;stroke-opacity:0.94881891;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

+46
View File
@@ -0,0 +1,46 @@
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" id="svg2" version="1.1" inkscape:version="0.91 r13725" sodipodi:docname="mpv-gradient.svg" viewBox="4.05 4.05 55.9 55.9">
<defs id="defs4">
<linearGradient inkscape:collect="always" id="linearGradient4430">
<stop style="stop-color:#65376e;stop-opacity:1" offset="0" id="stop4432"/>
<stop style="stop-color:#4c2354;stop-opacity:1" offset="1" id="stop4434"/>
</linearGradient>
<linearGradient id="linearGradient4418" inkscape:collect="always">
<stop id="stop4420" offset="0" style="stop-color:#e6e2e8;stop-opacity:1"/>
<stop id="stop4422" offset="1" style="stop-color:#aaa3ad;stop-opacity:1"/>
</linearGradient>
<linearGradient inkscape:collect="always" id="linearGradient4402">
<stop style="stop-color:#320f38;stop-opacity:1" offset="0" id="stop4404"/>
<stop style="stop-color:#5a2963;stop-opacity:1" offset="1" id="stop4406"/>
</linearGradient>
<linearGradient inkscape:collect="always" id="linearGradient4392">
<stop style="stop-color:#6b3c74;stop-opacity:1" offset="0" id="stop4394"/>
<stop style="stop-color:#461b4d;stop-opacity:1" offset="1" id="stop4396"/>
</linearGradient>
<linearGradient inkscape:collect="always" id="linearGradient4382">
<stop style="stop-color:#fafafa;stop-opacity:1" offset="0" id="stop4384"/>
<stop style="stop-color:#bababa;stop-opacity:1" offset="1" id="stop4386"/>
</linearGradient>
<linearGradient inkscape:collect="always" xlink:href="#linearGradient4382" id="linearGradient5278" gradientUnits="userSpaceOnUse" gradientTransform="translate(-86.873198,699.81912)" x1="97.187729" y1="305.67371" x2="140.38228" y2="335.64926"/>
<linearGradient inkscape:collect="always" xlink:href="#linearGradient4392" id="linearGradient5280" gradientUnits="userSpaceOnUse" gradientTransform="translate(-32.973108,701.2155)" x1="50.828064" y1="298.31949" x2="78.197021" y2="339.65219"/>
<linearGradient inkscape:collect="always" xlink:href="#linearGradient4402" id="linearGradient5282" gradientUnits="userSpaceOnUse" gradientTransform="translate(-31.48364,700.09839)" x1="53.620815" y1="305.76682" x2="77.824654" y2="331.73938"/>
<linearGradient inkscape:collect="always" xlink:href="#linearGradient4418" id="linearGradient5284" gradientUnits="userSpaceOnUse" gradientTransform="translate(-87.152474,700.0053)" x1="110.31366" y1="312.28323" x2="126.88398" y2="329.41211"/>
<linearGradient inkscape:collect="always" xlink:href="#linearGradient4430" id="linearGradient5286" gradientUnits="userSpaceOnUse" gradientTransform="translate(-87.152474,700.0053)" x1="115.97468" y1="317.41763" x2="119.37984" y2="322.43457"/>
</defs>
<sodipodi:namedview id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="5.3710484" inkscape:cx="-19.490294" inkscape:cy="18.643164" inkscape:document-units="px" inkscape:current-layer="layer1" showgrid="false" units="px" inkscape:window-width="1920" inkscape:window-height="1016" inkscape:window-x="0" inkscape:window-y="27" inkscape:window-maximized="1"/>
<metadata id="metadata7">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title/>
</cc:Work>
</rdf:RDF>
</metadata>
<g inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1" transform="translate(0,-988.3622)">
<circle style="opacity:1;fill:url(#linearGradient5278);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.10161044;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:1;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.99215686" id="path4380-0" cx="32" cy="1020.3622" r="27.949194"/>
<circle style="opacity:1;fill:url(#linearGradient5280);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0988237;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:1;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.99215686" id="path4390-3" cx="32.727058" cy="1019.5079" r="25.950588"/>
<circle style="opacity:1;fill:url(#linearGradient5282);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.1;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:1;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.99215686" id="path4400-3" cx="34.224396" cy="1017.7957" r="20"/>
<path style="fill:url(#linearGradient5284);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" d="m 44.481446,1020.4807 a 12.848894,12.848894 0 0 1 -12.84889,12.8489 12.848894,12.848894 0 0 1 -12.8489,-12.8489 12.848894,12.848894 0 0 1 12.8489,-12.8489 12.848894,12.848894 0 0 1 12.84889,12.8489 z" id="path4412-5" inkscape:connector-curvature="0"/>
<path style="fill:url(#linearGradient5286);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" d="m 28.374316,1014.709 0,11.4502 9.21608,-5.8647 z" id="path4426-2" inkscape:connector-curvature="0"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.5 KiB

+21
View File
@@ -0,0 +1,21 @@
<svg xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" version="1.0" id="Layer_1" x="0px" y="0px" style="enable-background:new 0 0 898 630.4;" xml:space="preserve" sodipodi:docname="mx_player.svg" inkscape:version="1.4 (e7c3feb1, 2024-10-09)" viewBox="74 224 182.4 182.4"><defs id="defs10"/><sodipodi:namedview id="namedview10" pagecolor="#ffffff" bordercolor="#000000" borderopacity="0.25" inkscape:showpageshadow="2" inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:zoom="0.41160677" inkscape:cx="170.06523" inkscape:cy="99.609634" inkscape:window-width="1472" inkscape:window-height="843" inkscape:window-x="0" inkscape:window-y="34" inkscape:window-maximized="1" inkscape:current-layer="Layer_1"/>
<style type="text/css" id="style1">
.st0{fill:#3B8BEF;}
.st1{fill:#010101;}
.st2{fill:#020202;}
.st3{fill:#040303;}
.st4{fill:#040404;}
.st5{fill:#060606;}
.st6{fill:#FFFFFF;}
</style>
<path class="st0" d="M256.4,315.3c0,50.3-40.8,91.1-91.1,91.1c-0.1,0-0.2,0-0.4,0c-51.2-0.2-91.2-42.1-90.9-91.8 c0.3-49.3,40.4-90.8,91.6-90.6C215.9,224.2,256.5,265.1,256.4,315.3z M137.2,317.5v37.8c0,0.5,0,1,0,1.5c0.1,0.9,0.8,1.5,1.7,1.5 c0,0,0.1,0,0.1,0c0.5-0.1,0.9-0.3,1.3-0.5l8.8-5l57.8-33.1c0.4-0.2,0.8-0.5,1.2-0.8c0.8-0.6,0.9-1.6,0.4-2.4 c-0.1-0.1-0.2-0.3-0.4-0.4c-0.3-0.2-0.6-0.4-0.9-0.5l-66.4-38c-0.5-0.3-0.9-0.5-1.4-0.8c-0.8-0.4-1.8-0.1-2.2,0.8 c-0.1,0.2-0.2,0.4-0.2,0.7c0,0.5,0,1,0,1.5L137.2,317.5z" id="path1"/>
<path class="st6" d="M137.2,317.5v-37.9c0-0.5,0-1,0-1.5c0.1-0.9,0.9-1.6,1.8-1.5c0.2,0,0.4,0.1,0.6,0.1c0.5,0.2,1,0.5,1.4,0.8 l66.4,38c0.3,0.2,0.6,0.3,0.9,0.5c0.8,0.6,0.9,1.6,0.4,2.4c-0.1,0.1-0.2,0.3-0.4,0.4c-0.4,0.3-0.8,0.6-1.2,0.8l-57.8,33.1l-8.8,5 c-0.4,0.3-0.9,0.5-1.4,0.6c-0.9,0.1-1.7-0.5-1.8-1.3c0,0,0-0.1,0-0.1c0-0.5,0-1,0-1.5L137.2,317.5z" id="path10"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 194 KiB

+17
View File
@@ -758,5 +758,22 @@
"hdr": "HDR",
"audioOutput": "Audioausgabe",
"performanceOverlay": "Leistungsanzeige"
},
"externalPlayer": {
"title": "Externer Player",
"useExternalPlayer": "Externen Player verwenden",
"useExternalPlayerDescription": "Videos in einer externen App statt im integrierten Player öffnen",
"selectPlayer": "Player auswählen",
"systemDefault": "Systemstandard",
"addCustomPlayer": "Benutzerdefinierten Player hinzufügen",
"playerName": "Playername",
"playerCommand": "Befehl",
"playerPackage": "Paketname",
"playerUrlScheme": "URL-Schema",
"customPlayer": "Benutzerdefinierter Player",
"off": "Aus",
"launchFailed": "Externer Player konnte nicht geöffnet werden",
"appNotInstalled": "${name} ist nicht installiert",
"playInExternalPlayer": "In externem Player abspielen"
}
}
+17
View File
@@ -758,5 +758,22 @@
"hdr": "HDR",
"audioOutput": "Audio Output",
"performanceOverlay": "Performance Overlay"
},
"externalPlayer": {
"title": "External Player",
"useExternalPlayer": "Use External Player",
"useExternalPlayerDescription": "Open videos in an external app instead of the built-in player",
"selectPlayer": "Select Player",
"systemDefault": "System Default",
"addCustomPlayer": "Add Custom Player",
"playerName": "Player Name",
"playerCommand": "Command",
"playerPackage": "Package Name",
"playerUrlScheme": "URL Scheme",
"customPlayer": "Custom Player",
"off": "Off",
"launchFailed": "Failed to open external player",
"appNotInstalled": "${name} is not installed",
"playInExternalPlayer": "Play in External Player"
}
}
+17
View File
@@ -758,5 +758,22 @@
"hdr": "HDR",
"audioOutput": "Salida de audio",
"performanceOverlay": "Indicador de rendimiento"
},
"externalPlayer": {
"title": "Reproductor externo",
"useExternalPlayer": "Usar reproductor externo",
"useExternalPlayerDescription": "Abrir vídeos en una app externa en lugar del reproductor integrado",
"selectPlayer": "Seleccionar reproductor",
"systemDefault": "Predeterminado del sistema",
"addCustomPlayer": "Añadir reproductor personalizado",
"playerName": "Nombre del reproductor",
"playerCommand": "Comando",
"playerPackage": "Nombre del paquete",
"playerUrlScheme": "Esquema URL",
"customPlayer": "Reproductor personalizado",
"off": "Desactivado",
"launchFailed": "No se pudo abrir el reproductor externo",
"appNotInstalled": "${name} no está instalado",
"playInExternalPlayer": "Reproducir en reproductor externo"
}
}
+17
View File
@@ -758,5 +758,22 @@
"hdr": "HDR",
"audioOutput": "Sortie audio",
"performanceOverlay": "Superposition de performance"
},
"externalPlayer": {
"title": "Lecteur externe",
"useExternalPlayer": "Utiliser un lecteur externe",
"useExternalPlayerDescription": "Ouvrir les vidéos dans une application externe au lieu du lecteur intégré",
"selectPlayer": "Sélectionner le lecteur",
"systemDefault": "Par défaut du système",
"addCustomPlayer": "Ajouter un lecteur personnalisé",
"playerName": "Nom du lecteur",
"playerCommand": "Commande",
"playerPackage": "Nom du paquet",
"playerUrlScheme": "Schéma URL",
"customPlayer": "Lecteur personnalisé",
"off": "Désactivé",
"launchFailed": "Impossible d'ouvrir le lecteur externe",
"appNotInstalled": "${name} n'est pas installé",
"playInExternalPlayer": "Lire dans un lecteur externe"
}
}
+17
View File
@@ -758,5 +758,22 @@
"hdr": "HDR",
"audioOutput": "Uscita audio",
"performanceOverlay": "Overlay prestazioni"
},
"externalPlayer": {
"title": "Lettore esterno",
"useExternalPlayer": "Usa lettore esterno",
"useExternalPlayerDescription": "Apri i video in un'app esterna invece del lettore integrato",
"selectPlayer": "Seleziona lettore",
"systemDefault": "Predefinito di sistema",
"addCustomPlayer": "Aggiungi lettore personalizzato",
"playerName": "Nome lettore",
"playerCommand": "Comando",
"playerPackage": "Nome pacchetto",
"playerUrlScheme": "Schema URL",
"customPlayer": "Lettore personalizzato",
"off": "Disattivato",
"launchFailed": "Impossibile aprire il lettore esterno",
"appNotInstalled": "${name} non è installato",
"playInExternalPlayer": "Riproduci in lettore esterno"
}
}
+17
View File
@@ -758,5 +758,22 @@
"hdr": "HDR",
"audioOutput": "오디오 출력",
"performanceOverlay": "성능 오버레이"
},
"externalPlayer": {
"title": "외부 플레이어",
"useExternalPlayer": "외부 플레이어 사용",
"useExternalPlayerDescription": "내장 플레이어 대신 외부 앱에서 동영상 열기",
"selectPlayer": "플레이어 선택",
"systemDefault": "시스템 기본값",
"addCustomPlayer": "사용자 정의 플레이어 추가",
"playerName": "플레이어 이름",
"playerCommand": "명령어",
"playerPackage": "패키지 이름",
"playerUrlScheme": "URL 스킴",
"customPlayer": "사용자 정의 플레이어",
"off": "꺼짐",
"launchFailed": "외부 플레이어를 열 수 없습니다",
"appNotInstalled": "${name}이(가) 설치되어 있지 않습니다",
"playInExternalPlayer": "외부 플레이어에서 재생"
}
}
+17
View File
@@ -758,5 +758,22 @@
"hdr": "HDR",
"audioOutput": "Audio-uitvoer",
"performanceOverlay": "Prestatie-overlay"
},
"externalPlayer": {
"title": "Externe speler",
"useExternalPlayer": "Externe speler gebruiken",
"useExternalPlayerDescription": "Open video's in een externe app in plaats van de ingebouwde speler",
"selectPlayer": "Speler selecteren",
"systemDefault": "Systeemstandaard",
"addCustomPlayer": "Aangepaste speler toevoegen",
"playerName": "Spelernaam",
"playerCommand": "Commando",
"playerPackage": "Pakketnaam",
"playerUrlScheme": "URL-schema",
"customPlayer": "Aangepaste speler",
"off": "Uit",
"launchFailed": "Kan externe speler niet openen",
"appNotInstalled": "${name} is niet geïnstalleerd",
"playInExternalPlayer": "Afspelen in externe speler"
}
}
+2 -2
View File
@@ -4,9 +4,9 @@
/// To regenerate, run: `dart run slang`
///
/// Locales: 9
/// Strings: 6084 (676 per locale)
/// Strings: 6219 (691 per locale)
///
/// Built on 2026-02-10 at 21:45 UTC
/// Built on 2026-02-11 at 13:27 UTC
// coverage:ignore-file
// ignore_for_file: type=lint, unused_import
+40
View File
@@ -72,6 +72,7 @@ class TranslationsDe with BaseTranslations<AppLocale, Translations> implements T
@override late final _TranslationsShadersDe shaders = _TranslationsShadersDe._(_root);
@override late final _TranslationsCompanionRemoteDe companionRemote = _TranslationsCompanionRemoteDe._(_root);
@override late final _TranslationsVideoSettingsDe videoSettings = _TranslationsVideoSettingsDe._(_root);
@override late final _TranslationsExternalPlayerDe externalPlayer = _TranslationsExternalPlayerDe._(_root);
}
// Path: app
@@ -961,6 +962,30 @@ class _TranslationsVideoSettingsDe implements TranslationsVideoSettingsEn {
@override String get performanceOverlay => 'Leistungsanzeige';
}
// Path: externalPlayer
class _TranslationsExternalPlayerDe implements TranslationsExternalPlayerEn {
_TranslationsExternalPlayerDe._(this._root);
final TranslationsDe _root; // ignore: unused_field
// Translations
@override String get title => 'Externer Player';
@override String get useExternalPlayer => 'Externen Player verwenden';
@override String get useExternalPlayerDescription => 'Videos in einer externen App statt im integrierten Player öffnen';
@override String get selectPlayer => 'Player auswählen';
@override String get systemDefault => 'Systemstandard';
@override String get addCustomPlayer => 'Benutzerdefinierten Player hinzufügen';
@override String get playerName => 'Playername';
@override String get playerCommand => 'Befehl';
@override String get playerPackage => 'Paketname';
@override String get playerUrlScheme => 'URL-Schema';
@override String get customPlayer => 'Benutzerdefinierter Player';
@override String get off => 'Aus';
@override String get launchFailed => 'Externer Player konnte nicht geöffnet werden';
@override String appNotInstalled({required Object name}) => '${name} ist nicht installiert';
@override String get playInExternalPlayer => 'In externem Player abspielen';
}
// Path: hotkeys.actions
class _TranslationsHotkeysActionsDe implements TranslationsHotkeysActionsEn {
_TranslationsHotkeysActionsDe._(this._root);
@@ -1821,6 +1846,21 @@ extension on TranslationsDe {
'videoSettings.hdr' => 'HDR',
'videoSettings.audioOutput' => 'Audioausgabe',
'videoSettings.performanceOverlay' => 'Leistungsanzeige',
'externalPlayer.title' => 'Externer Player',
'externalPlayer.useExternalPlayer' => 'Externen Player verwenden',
'externalPlayer.useExternalPlayerDescription' => 'Videos in einer externen App statt im integrierten Player öffnen',
'externalPlayer.selectPlayer' => 'Player auswählen',
'externalPlayer.systemDefault' => 'Systemstandard',
'externalPlayer.addCustomPlayer' => 'Benutzerdefinierten Player hinzufügen',
'externalPlayer.playerName' => 'Playername',
'externalPlayer.playerCommand' => 'Befehl',
'externalPlayer.playerPackage' => 'Paketname',
'externalPlayer.playerUrlScheme' => 'URL-Schema',
'externalPlayer.customPlayer' => 'Benutzerdefinierter Player',
'externalPlayer.off' => 'Aus',
'externalPlayer.launchFailed' => 'Externer Player konnte nicht geöffnet werden',
'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} ist nicht installiert',
'externalPlayer.playInExternalPlayer' => 'In externem Player abspielen',
_ => null,
};
}
+70
View File
@@ -75,6 +75,7 @@ class Translations with BaseTranslations<AppLocale, Translations> {
late final TranslationsShadersEn shaders = TranslationsShadersEn._(_root);
late final TranslationsCompanionRemoteEn companionRemote = TranslationsCompanionRemoteEn._(_root);
late final TranslationsVideoSettingsEn videoSettings = TranslationsVideoSettingsEn._(_root);
late final TranslationsExternalPlayerEn externalPlayer = TranslationsExternalPlayerEn._(_root);
}
// Path: app
@@ -2098,6 +2099,60 @@ class TranslationsVideoSettingsEn {
String get performanceOverlay => 'Performance Overlay';
}
// Path: externalPlayer
class TranslationsExternalPlayerEn {
TranslationsExternalPlayerEn._(this._root);
final Translations _root; // ignore: unused_field
// Translations
/// en: 'External Player'
String get title => 'External Player';
/// en: 'Use External Player'
String get useExternalPlayer => 'Use External Player';
/// en: 'Open videos in an external app instead of the built-in player'
String get useExternalPlayerDescription => 'Open videos in an external app instead of the built-in player';
/// en: 'Select Player'
String get selectPlayer => 'Select Player';
/// en: 'System Default'
String get systemDefault => 'System Default';
/// en: 'Add Custom Player'
String get addCustomPlayer => 'Add Custom Player';
/// en: 'Player Name'
String get playerName => 'Player Name';
/// en: 'Command'
String get playerCommand => 'Command';
/// en: 'Package Name'
String get playerPackage => 'Package Name';
/// en: 'URL Scheme'
String get playerUrlScheme => 'URL Scheme';
/// en: 'Custom Player'
String get customPlayer => 'Custom Player';
/// en: 'Off'
String get off => 'Off';
/// en: 'Failed to open external player'
String get launchFailed => 'Failed to open external player';
/// en: '${name} is not installed'
String appNotInstalled({required Object name}) => '${name} is not installed';
/// en: 'Play in External Player'
String get playInExternalPlayer => 'Play in External Player';
}
// Path: hotkeys.actions
class TranslationsHotkeysActionsEn {
TranslationsHotkeysActionsEn._(this._root);
@@ -3180,6 +3235,21 @@ extension on Translations {
'videoSettings.hdr' => 'HDR',
'videoSettings.audioOutput' => 'Audio Output',
'videoSettings.performanceOverlay' => 'Performance Overlay',
'externalPlayer.title' => 'External Player',
'externalPlayer.useExternalPlayer' => 'Use External Player',
'externalPlayer.useExternalPlayerDescription' => 'Open videos in an external app instead of the built-in player',
'externalPlayer.selectPlayer' => 'Select Player',
'externalPlayer.systemDefault' => 'System Default',
'externalPlayer.addCustomPlayer' => 'Add Custom Player',
'externalPlayer.playerName' => 'Player Name',
'externalPlayer.playerCommand' => 'Command',
'externalPlayer.playerPackage' => 'Package Name',
'externalPlayer.playerUrlScheme' => 'URL Scheme',
'externalPlayer.customPlayer' => 'Custom Player',
'externalPlayer.off' => 'Off',
'externalPlayer.launchFailed' => 'Failed to open external player',
'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} is not installed',
'externalPlayer.playInExternalPlayer' => 'Play in External Player',
_ => null,
};
}
+40
View File
@@ -72,6 +72,7 @@ class TranslationsEs with BaseTranslations<AppLocale, Translations> implements T
@override late final _TranslationsShadersEs shaders = _TranslationsShadersEs._(_root);
@override late final _TranslationsCompanionRemoteEs companionRemote = _TranslationsCompanionRemoteEs._(_root);
@override late final _TranslationsVideoSettingsEs videoSettings = _TranslationsVideoSettingsEs._(_root);
@override late final _TranslationsExternalPlayerEs externalPlayer = _TranslationsExternalPlayerEs._(_root);
}
// Path: app
@@ -961,6 +962,30 @@ class _TranslationsVideoSettingsEs implements TranslationsVideoSettingsEn {
@override String get performanceOverlay => 'Indicador de rendimiento';
}
// Path: externalPlayer
class _TranslationsExternalPlayerEs implements TranslationsExternalPlayerEn {
_TranslationsExternalPlayerEs._(this._root);
final TranslationsEs _root; // ignore: unused_field
// Translations
@override String get title => 'Reproductor externo';
@override String get useExternalPlayer => 'Usar reproductor externo';
@override String get useExternalPlayerDescription => 'Abrir vídeos en una app externa en lugar del reproductor integrado';
@override String get selectPlayer => 'Seleccionar reproductor';
@override String get systemDefault => 'Predeterminado del sistema';
@override String get addCustomPlayer => 'Añadir reproductor personalizado';
@override String get playerName => 'Nombre del reproductor';
@override String get playerCommand => 'Comando';
@override String get playerPackage => 'Nombre del paquete';
@override String get playerUrlScheme => 'Esquema URL';
@override String get customPlayer => 'Reproductor personalizado';
@override String get off => 'Desactivado';
@override String get launchFailed => 'No se pudo abrir el reproductor externo';
@override String appNotInstalled({required Object name}) => '${name} no está instalado';
@override String get playInExternalPlayer => 'Reproducir en reproductor externo';
}
// Path: hotkeys.actions
class _TranslationsHotkeysActionsEs implements TranslationsHotkeysActionsEn {
_TranslationsHotkeysActionsEs._(this._root);
@@ -1821,6 +1846,21 @@ extension on TranslationsEs {
'videoSettings.hdr' => 'HDR',
'videoSettings.audioOutput' => 'Salida de audio',
'videoSettings.performanceOverlay' => 'Indicador de rendimiento',
'externalPlayer.title' => 'Reproductor externo',
'externalPlayer.useExternalPlayer' => 'Usar reproductor externo',
'externalPlayer.useExternalPlayerDescription' => 'Abrir vídeos en una app externa en lugar del reproductor integrado',
'externalPlayer.selectPlayer' => 'Seleccionar reproductor',
'externalPlayer.systemDefault' => 'Predeterminado del sistema',
'externalPlayer.addCustomPlayer' => 'Añadir reproductor personalizado',
'externalPlayer.playerName' => 'Nombre del reproductor',
'externalPlayer.playerCommand' => 'Comando',
'externalPlayer.playerPackage' => 'Nombre del paquete',
'externalPlayer.playerUrlScheme' => 'Esquema URL',
'externalPlayer.customPlayer' => 'Reproductor personalizado',
'externalPlayer.off' => 'Desactivado',
'externalPlayer.launchFailed' => 'No se pudo abrir el reproductor externo',
'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} no está instalado',
'externalPlayer.playInExternalPlayer' => 'Reproducir en reproductor externo',
_ => null,
};
}
+40
View File
@@ -72,6 +72,7 @@ class TranslationsFr with BaseTranslations<AppLocale, Translations> implements T
@override late final _TranslationsShadersFr shaders = _TranslationsShadersFr._(_root);
@override late final _TranslationsCompanionRemoteFr companionRemote = _TranslationsCompanionRemoteFr._(_root);
@override late final _TranslationsVideoSettingsFr videoSettings = _TranslationsVideoSettingsFr._(_root);
@override late final _TranslationsExternalPlayerFr externalPlayer = _TranslationsExternalPlayerFr._(_root);
}
// Path: app
@@ -961,6 +962,30 @@ class _TranslationsVideoSettingsFr implements TranslationsVideoSettingsEn {
@override String get performanceOverlay => 'Superposition de performance';
}
// Path: externalPlayer
class _TranslationsExternalPlayerFr implements TranslationsExternalPlayerEn {
_TranslationsExternalPlayerFr._(this._root);
final TranslationsFr _root; // ignore: unused_field
// Translations
@override String get title => 'Lecteur externe';
@override String get useExternalPlayer => 'Utiliser un lecteur externe';
@override String get useExternalPlayerDescription => 'Ouvrir les vidéos dans une application externe au lieu du lecteur intégré';
@override String get selectPlayer => 'Sélectionner le lecteur';
@override String get systemDefault => 'Par défaut du système';
@override String get addCustomPlayer => 'Ajouter un lecteur personnalisé';
@override String get playerName => 'Nom du lecteur';
@override String get playerCommand => 'Commande';
@override String get playerPackage => 'Nom du paquet';
@override String get playerUrlScheme => 'Schéma URL';
@override String get customPlayer => 'Lecteur personnalisé';
@override String get off => 'Désactivé';
@override String get launchFailed => 'Impossible d\'ouvrir le lecteur externe';
@override String appNotInstalled({required Object name}) => '${name} n\'est pas installé';
@override String get playInExternalPlayer => 'Lire dans un lecteur externe';
}
// Path: hotkeys.actions
class _TranslationsHotkeysActionsFr implements TranslationsHotkeysActionsEn {
_TranslationsHotkeysActionsFr._(this._root);
@@ -1821,6 +1846,21 @@ extension on TranslationsFr {
'videoSettings.hdr' => 'HDR',
'videoSettings.audioOutput' => 'Sortie audio',
'videoSettings.performanceOverlay' => 'Superposition de performance',
'externalPlayer.title' => 'Lecteur externe',
'externalPlayer.useExternalPlayer' => 'Utiliser un lecteur externe',
'externalPlayer.useExternalPlayerDescription' => 'Ouvrir les vidéos dans une application externe au lieu du lecteur intégré',
'externalPlayer.selectPlayer' => 'Sélectionner le lecteur',
'externalPlayer.systemDefault' => 'Par défaut du système',
'externalPlayer.addCustomPlayer' => 'Ajouter un lecteur personnalisé',
'externalPlayer.playerName' => 'Nom du lecteur',
'externalPlayer.playerCommand' => 'Commande',
'externalPlayer.playerPackage' => 'Nom du paquet',
'externalPlayer.playerUrlScheme' => 'Schéma URL',
'externalPlayer.customPlayer' => 'Lecteur personnalisé',
'externalPlayer.off' => 'Désactivé',
'externalPlayer.launchFailed' => 'Impossible d\'ouvrir le lecteur externe',
'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} n\'est pas installé',
'externalPlayer.playInExternalPlayer' => 'Lire dans un lecteur externe',
_ => null,
};
}
+40
View File
@@ -72,6 +72,7 @@ class TranslationsIt with BaseTranslations<AppLocale, Translations> implements T
@override late final _TranslationsShadersIt shaders = _TranslationsShadersIt._(_root);
@override late final _TranslationsCompanionRemoteIt companionRemote = _TranslationsCompanionRemoteIt._(_root);
@override late final _TranslationsVideoSettingsIt videoSettings = _TranslationsVideoSettingsIt._(_root);
@override late final _TranslationsExternalPlayerIt externalPlayer = _TranslationsExternalPlayerIt._(_root);
}
// Path: app
@@ -961,6 +962,30 @@ class _TranslationsVideoSettingsIt implements TranslationsVideoSettingsEn {
@override String get performanceOverlay => 'Overlay prestazioni';
}
// Path: externalPlayer
class _TranslationsExternalPlayerIt implements TranslationsExternalPlayerEn {
_TranslationsExternalPlayerIt._(this._root);
final TranslationsIt _root; // ignore: unused_field
// Translations
@override String get title => 'Lettore esterno';
@override String get useExternalPlayer => 'Usa lettore esterno';
@override String get useExternalPlayerDescription => 'Apri i video in un\'app esterna invece del lettore integrato';
@override String get selectPlayer => 'Seleziona lettore';
@override String get systemDefault => 'Predefinito di sistema';
@override String get addCustomPlayer => 'Aggiungi lettore personalizzato';
@override String get playerName => 'Nome lettore';
@override String get playerCommand => 'Comando';
@override String get playerPackage => 'Nome pacchetto';
@override String get playerUrlScheme => 'Schema URL';
@override String get customPlayer => 'Lettore personalizzato';
@override String get off => 'Disattivato';
@override String get launchFailed => 'Impossibile aprire il lettore esterno';
@override String appNotInstalled({required Object name}) => '${name} non è installato';
@override String get playInExternalPlayer => 'Riproduci in lettore esterno';
}
// Path: hotkeys.actions
class _TranslationsHotkeysActionsIt implements TranslationsHotkeysActionsEn {
_TranslationsHotkeysActionsIt._(this._root);
@@ -1821,6 +1846,21 @@ extension on TranslationsIt {
'videoSettings.hdr' => 'HDR',
'videoSettings.audioOutput' => 'Uscita audio',
'videoSettings.performanceOverlay' => 'Overlay prestazioni',
'externalPlayer.title' => 'Lettore esterno',
'externalPlayer.useExternalPlayer' => 'Usa lettore esterno',
'externalPlayer.useExternalPlayerDescription' => 'Apri i video in un\'app esterna invece del lettore integrato',
'externalPlayer.selectPlayer' => 'Seleziona lettore',
'externalPlayer.systemDefault' => 'Predefinito di sistema',
'externalPlayer.addCustomPlayer' => 'Aggiungi lettore personalizzato',
'externalPlayer.playerName' => 'Nome lettore',
'externalPlayer.playerCommand' => 'Comando',
'externalPlayer.playerPackage' => 'Nome pacchetto',
'externalPlayer.playerUrlScheme' => 'Schema URL',
'externalPlayer.customPlayer' => 'Lettore personalizzato',
'externalPlayer.off' => 'Disattivato',
'externalPlayer.launchFailed' => 'Impossibile aprire il lettore esterno',
'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} non è installato',
'externalPlayer.playInExternalPlayer' => 'Riproduci in lettore esterno',
_ => null,
};
}
+40
View File
@@ -72,6 +72,7 @@ class TranslationsKo with BaseTranslations<AppLocale, Translations> implements T
@override late final _TranslationsShadersKo shaders = _TranslationsShadersKo._(_root);
@override late final _TranslationsCompanionRemoteKo companionRemote = _TranslationsCompanionRemoteKo._(_root);
@override late final _TranslationsVideoSettingsKo videoSettings = _TranslationsVideoSettingsKo._(_root);
@override late final _TranslationsExternalPlayerKo externalPlayer = _TranslationsExternalPlayerKo._(_root);
}
// Path: app
@@ -961,6 +962,30 @@ class _TranslationsVideoSettingsKo implements TranslationsVideoSettingsEn {
@override String get performanceOverlay => '성능 오버레이';
}
// Path: externalPlayer
class _TranslationsExternalPlayerKo implements TranslationsExternalPlayerEn {
_TranslationsExternalPlayerKo._(this._root);
final TranslationsKo _root; // ignore: unused_field
// Translations
@override String get title => '외부 플레이어';
@override String get useExternalPlayer => '외부 플레이어 사용';
@override String get useExternalPlayerDescription => '내장 플레이어 대신 외부 앱에서 동영상 열기';
@override String get selectPlayer => '플레이어 선택';
@override String get systemDefault => '시스템 기본값';
@override String get addCustomPlayer => '사용자 정의 플레이어 추가';
@override String get playerName => '플레이어 이름';
@override String get playerCommand => '명령어';
@override String get playerPackage => '패키지 이름';
@override String get playerUrlScheme => 'URL 스킴';
@override String get customPlayer => '사용자 정의 플레이어';
@override String get off => '꺼짐';
@override String get launchFailed => '외부 플레이어를 열 수 없습니다';
@override String appNotInstalled({required Object name}) => '${name}이(가) 설치되어 있지 않습니다';
@override String get playInExternalPlayer => '외부 플레이어에서 재생';
}
// Path: hotkeys.actions
class _TranslationsHotkeysActionsKo implements TranslationsHotkeysActionsEn {
_TranslationsHotkeysActionsKo._(this._root);
@@ -1821,6 +1846,21 @@ extension on TranslationsKo {
'videoSettings.hdr' => 'HDR',
'videoSettings.audioOutput' => '오디오 출력',
'videoSettings.performanceOverlay' => '성능 오버레이',
'externalPlayer.title' => '외부 플레이어',
'externalPlayer.useExternalPlayer' => '외부 플레이어 사용',
'externalPlayer.useExternalPlayerDescription' => '내장 플레이어 대신 외부 앱에서 동영상 열기',
'externalPlayer.selectPlayer' => '플레이어 선택',
'externalPlayer.systemDefault' => '시스템 기본값',
'externalPlayer.addCustomPlayer' => '사용자 정의 플레이어 추가',
'externalPlayer.playerName' => '플레이어 이름',
'externalPlayer.playerCommand' => '명령어',
'externalPlayer.playerPackage' => '패키지 이름',
'externalPlayer.playerUrlScheme' => 'URL 스킴',
'externalPlayer.customPlayer' => '사용자 정의 플레이어',
'externalPlayer.off' => '꺼짐',
'externalPlayer.launchFailed' => '외부 플레이어를 열 수 없습니다',
'externalPlayer.appNotInstalled' => ({required Object name}) => '${name}이(가) 설치되어 있지 않습니다',
'externalPlayer.playInExternalPlayer' => '외부 플레이어에서 재생',
_ => null,
};
}
+40
View File
@@ -72,6 +72,7 @@ class TranslationsNl with BaseTranslations<AppLocale, Translations> implements T
@override late final _TranslationsShadersNl shaders = _TranslationsShadersNl._(_root);
@override late final _TranslationsCompanionRemoteNl companionRemote = _TranslationsCompanionRemoteNl._(_root);
@override late final _TranslationsVideoSettingsNl videoSettings = _TranslationsVideoSettingsNl._(_root);
@override late final _TranslationsExternalPlayerNl externalPlayer = _TranslationsExternalPlayerNl._(_root);
}
// Path: app
@@ -961,6 +962,30 @@ class _TranslationsVideoSettingsNl implements TranslationsVideoSettingsEn {
@override String get performanceOverlay => 'Prestatie-overlay';
}
// Path: externalPlayer
class _TranslationsExternalPlayerNl implements TranslationsExternalPlayerEn {
_TranslationsExternalPlayerNl._(this._root);
final TranslationsNl _root; // ignore: unused_field
// Translations
@override String get title => 'Externe speler';
@override String get useExternalPlayer => 'Externe speler gebruiken';
@override String get useExternalPlayerDescription => 'Open video\'s in een externe app in plaats van de ingebouwde speler';
@override String get selectPlayer => 'Speler selecteren';
@override String get systemDefault => 'Systeemstandaard';
@override String get addCustomPlayer => 'Aangepaste speler toevoegen';
@override String get playerName => 'Spelernaam';
@override String get playerCommand => 'Commando';
@override String get playerPackage => 'Pakketnaam';
@override String get playerUrlScheme => 'URL-schema';
@override String get customPlayer => 'Aangepaste speler';
@override String get off => 'Uit';
@override String get launchFailed => 'Kan externe speler niet openen';
@override String appNotInstalled({required Object name}) => '${name} is niet geïnstalleerd';
@override String get playInExternalPlayer => 'Afspelen in externe speler';
}
// Path: hotkeys.actions
class _TranslationsHotkeysActionsNl implements TranslationsHotkeysActionsEn {
_TranslationsHotkeysActionsNl._(this._root);
@@ -1821,6 +1846,21 @@ extension on TranslationsNl {
'videoSettings.hdr' => 'HDR',
'videoSettings.audioOutput' => 'Audio-uitvoer',
'videoSettings.performanceOverlay' => 'Prestatie-overlay',
'externalPlayer.title' => 'Externe speler',
'externalPlayer.useExternalPlayer' => 'Externe speler gebruiken',
'externalPlayer.useExternalPlayerDescription' => 'Open video\'s in een externe app in plaats van de ingebouwde speler',
'externalPlayer.selectPlayer' => 'Speler selecteren',
'externalPlayer.systemDefault' => 'Systeemstandaard',
'externalPlayer.addCustomPlayer' => 'Aangepaste speler toevoegen',
'externalPlayer.playerName' => 'Spelernaam',
'externalPlayer.playerCommand' => 'Commando',
'externalPlayer.playerPackage' => 'Pakketnaam',
'externalPlayer.playerUrlScheme' => 'URL-schema',
'externalPlayer.customPlayer' => 'Aangepaste speler',
'externalPlayer.off' => 'Uit',
'externalPlayer.launchFailed' => 'Kan externe speler niet openen',
'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} is niet geïnstalleerd',
'externalPlayer.playInExternalPlayer' => 'Afspelen in externe speler',
_ => null,
};
}
+40
View File
@@ -72,6 +72,7 @@ class TranslationsSv with BaseTranslations<AppLocale, Translations> implements T
@override late final _TranslationsShadersSv shaders = _TranslationsShadersSv._(_root);
@override late final _TranslationsCompanionRemoteSv companionRemote = _TranslationsCompanionRemoteSv._(_root);
@override late final _TranslationsVideoSettingsSv videoSettings = _TranslationsVideoSettingsSv._(_root);
@override late final _TranslationsExternalPlayerSv externalPlayer = _TranslationsExternalPlayerSv._(_root);
}
// Path: app
@@ -961,6 +962,30 @@ class _TranslationsVideoSettingsSv implements TranslationsVideoSettingsEn {
@override String get performanceOverlay => 'Prestandaöverlägg';
}
// Path: externalPlayer
class _TranslationsExternalPlayerSv implements TranslationsExternalPlayerEn {
_TranslationsExternalPlayerSv._(this._root);
final TranslationsSv _root; // ignore: unused_field
// Translations
@override String get title => 'Extern spelare';
@override String get useExternalPlayer => 'Använd extern spelare';
@override String get useExternalPlayerDescription => 'Öppna videor i en extern app istället för den inbyggda spelaren';
@override String get selectPlayer => 'Välj spelare';
@override String get systemDefault => 'Systemstandard';
@override String get addCustomPlayer => 'Lägg till anpassad spelare';
@override String get playerName => 'Spelarnamn';
@override String get playerCommand => 'Kommando';
@override String get playerPackage => 'Paketnamn';
@override String get playerUrlScheme => 'URL-schema';
@override String get customPlayer => 'Anpassad spelare';
@override String get off => 'Av';
@override String get launchFailed => 'Kunde inte öppna extern spelare';
@override String appNotInstalled({required Object name}) => '${name} är inte installerad';
@override String get playInExternalPlayer => 'Spela i extern spelare';
}
// Path: hotkeys.actions
class _TranslationsHotkeysActionsSv implements TranslationsHotkeysActionsEn {
_TranslationsHotkeysActionsSv._(this._root);
@@ -1821,6 +1846,21 @@ extension on TranslationsSv {
'videoSettings.hdr' => 'HDR',
'videoSettings.audioOutput' => 'Ljudutgång',
'videoSettings.performanceOverlay' => 'Prestandaöverlägg',
'externalPlayer.title' => 'Extern spelare',
'externalPlayer.useExternalPlayer' => 'Använd extern spelare',
'externalPlayer.useExternalPlayerDescription' => 'Öppna videor i en extern app istället för den inbyggda spelaren',
'externalPlayer.selectPlayer' => 'Välj spelare',
'externalPlayer.systemDefault' => 'Systemstandard',
'externalPlayer.addCustomPlayer' => 'Lägg till anpassad spelare',
'externalPlayer.playerName' => 'Spelarnamn',
'externalPlayer.playerCommand' => 'Kommando',
'externalPlayer.playerPackage' => 'Paketnamn',
'externalPlayer.playerUrlScheme' => 'URL-schema',
'externalPlayer.customPlayer' => 'Anpassad spelare',
'externalPlayer.off' => 'Av',
'externalPlayer.launchFailed' => 'Kunde inte öppna extern spelare',
'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} är inte installerad',
'externalPlayer.playInExternalPlayer' => 'Spela i extern spelare',
_ => null,
};
}
+40
View File
@@ -72,6 +72,7 @@ class TranslationsZh with BaseTranslations<AppLocale, Translations> implements T
@override late final _TranslationsShadersZh shaders = _TranslationsShadersZh._(_root);
@override late final _TranslationsCompanionRemoteZh companionRemote = _TranslationsCompanionRemoteZh._(_root);
@override late final _TranslationsVideoSettingsZh videoSettings = _TranslationsVideoSettingsZh._(_root);
@override late final _TranslationsExternalPlayerZh externalPlayer = _TranslationsExternalPlayerZh._(_root);
}
// Path: app
@@ -961,6 +962,30 @@ class _TranslationsVideoSettingsZh implements TranslationsVideoSettingsEn {
@override String get performanceOverlay => '性能监控';
}
// Path: externalPlayer
class _TranslationsExternalPlayerZh implements TranslationsExternalPlayerEn {
_TranslationsExternalPlayerZh._(this._root);
final TranslationsZh _root; // ignore: unused_field
// Translations
@override String get title => '外部播放器';
@override String get useExternalPlayer => '使用外部播放器';
@override String get useExternalPlayerDescription => '在外部应用中打开视频,而不是使用内置播放器';
@override String get selectPlayer => '选择播放器';
@override String get systemDefault => '系统默认';
@override String get addCustomPlayer => '添加自定义播放器';
@override String get playerName => '播放器名称';
@override String get playerCommand => '命令';
@override String get playerPackage => '包名';
@override String get playerUrlScheme => 'URL 方案';
@override String get customPlayer => '自定义播放器';
@override String get off => '关闭';
@override String get launchFailed => '无法打开外部播放器';
@override String appNotInstalled({required Object name}) => '${name} 未安装';
@override String get playInExternalPlayer => '在外部播放器中播放';
}
// Path: hotkeys.actions
class _TranslationsHotkeysActionsZh implements TranslationsHotkeysActionsEn {
_TranslationsHotkeysActionsZh._(this._root);
@@ -1821,6 +1846,21 @@ extension on TranslationsZh {
'videoSettings.hdr' => 'HDR',
'videoSettings.audioOutput' => '音频输出',
'videoSettings.performanceOverlay' => '性能监控',
'externalPlayer.title' => '外部播放器',
'externalPlayer.useExternalPlayer' => '使用外部播放器',
'externalPlayer.useExternalPlayerDescription' => '在外部应用中打开视频,而不是使用内置播放器',
'externalPlayer.selectPlayer' => '选择播放器',
'externalPlayer.systemDefault' => '系统默认',
'externalPlayer.addCustomPlayer' => '添加自定义播放器',
'externalPlayer.playerName' => '播放器名称',
'externalPlayer.playerCommand' => '命令',
'externalPlayer.playerPackage' => '包名',
'externalPlayer.playerUrlScheme' => 'URL 方案',
'externalPlayer.customPlayer' => '自定义播放器',
'externalPlayer.off' => '关闭',
'externalPlayer.launchFailed' => '无法打开外部播放器',
'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} 未安装',
'externalPlayer.playInExternalPlayer' => '在外部播放器中播放',
_ => null,
};
}
+17
View File
@@ -758,5 +758,22 @@
"hdr": "HDR",
"audioOutput": "Ljudutgång",
"performanceOverlay": "Prestandaöverlägg"
},
"externalPlayer": {
"title": "Extern spelare",
"useExternalPlayer": "Använd extern spelare",
"useExternalPlayerDescription": "Öppna videor i en extern app istället för den inbyggda spelaren",
"selectPlayer": "Välj spelare",
"systemDefault": "Systemstandard",
"addCustomPlayer": "Lägg till anpassad spelare",
"playerName": "Spelarnamn",
"playerCommand": "Kommando",
"playerPackage": "Paketnamn",
"playerUrlScheme": "URL-schema",
"customPlayer": "Anpassad spelare",
"off": "Av",
"launchFailed": "Kunde inte öppna extern spelare",
"appNotInstalled": "${name} är inte installerad",
"playInExternalPlayer": "Spela i extern spelare"
}
}
+17
View File
@@ -758,5 +758,22 @@
"hdr": "HDR",
"audioOutput": "音频输出",
"performanceOverlay": "性能监控"
},
"externalPlayer": {
"title": "外部播放器",
"useExternalPlayer": "使用外部播放器",
"useExternalPlayerDescription": "在外部应用中打开视频,而不是使用内置播放器",
"selectPlayer": "选择播放器",
"systemDefault": "系统默认",
"addCustomPlayer": "添加自定义播放器",
"playerName": "播放器名称",
"playerCommand": "命令",
"playerPackage": "包名",
"playerUrlScheme": "URL 方案",
"customPlayer": "自定义播放器",
"off": "关闭",
"launchFailed": "无法打开外部播放器",
"appNotInstalled": "${name} 未安装",
"playInExternalPlayer": "在外部播放器中播放"
}
}
+238
View File
@@ -0,0 +1,238 @@
import 'dart:convert';
import 'dart:io';
import 'package:url_launcher/url_launcher.dart';
import '../utils/app_logger.dart';
typedef PlayerLauncher = Future<bool> Function(String url);
enum CustomPlayerType { command, urlScheme }
class ExternalPlayer {
final String id;
final String name;
final String? iconAsset;
final bool isAvailable;
final PlayerLauncher launch;
final bool isCustom;
final String? customValue; // Only for custom player serialization
final CustomPlayerType? customType;
ExternalPlayer({
required this.id,
required this.name,
this.iconAsset,
this.isAvailable = true,
required this.launch,
this.isCustom = false,
this.customValue,
this.customType,
});
factory ExternalPlayer.custom({
required String id,
required String name,
required String value,
required CustomPlayerType type,
}) {
return ExternalPlayer(
id: id,
name: name,
isCustom: true,
customValue: value,
customType: type,
launch: (url) => _launchCustom(value, url, type),
);
}
factory ExternalPlayer.fromJson(Map<String, dynamic> json) {
final id = json['id'] as String;
final known = KnownPlayers.findById(id);
if (known != null) return known;
return ExternalPlayer.custom(
id: id,
name: json['name'] as String,
value: json['customValue'] as String? ?? '',
type: json['customType'] == 'urlScheme' ? CustomPlayerType.urlScheme : CustomPlayerType.command,
);
}
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
if (isCustom) 'isCustom': true,
if (customValue != null) 'customValue': customValue,
if (customType != null) 'customType': customType == CustomPlayerType.urlScheme ? 'urlScheme' : 'command',
};
String toJsonString() => json.encode(toJson());
static ExternalPlayer fromJsonString(String jsonString) =>
ExternalPlayer.fromJson(json.decode(jsonString) as Map<String, dynamic>);
@override
bool operator ==(Object other) =>
identical(this, other) || other is ExternalPlayer && runtimeType == other.runtimeType && id == other.id;
@override
int get hashCode => id.hashCode;
}
// --- Launch helpers ---
Future<bool> _launchWithUrl(String url) async {
return launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication);
}
Future<bool> _launchAndroidIntent(String url, {required String package}) async {
final intentUri = Uri.parse(
'intent:$url#Intent;'
'action=android.intent.action.VIEW;'
'type=video/*;'
'package=$package;'
'end',
);
try {
return await launchUrl(intentUri, mode: LaunchMode.externalApplication);
} catch (_) {
return _launchWithUrl(url);
}
}
Future<bool> _launchUrlScheme(String scheme, String url) async {
final playerUrl = scheme.contains('url=') ? '$scheme${Uri.encodeComponent(url)}' : '$scheme$url';
final uri = Uri.parse(playerUrl);
if (await canLaunchUrl(uri)) {
return launchUrl(uri, mode: LaunchMode.externalApplication);
}
return false;
}
Future<bool> _launchMacApp(String appName, String url) async {
try {
await Process.start('open', ['-a', appName, url], mode: ProcessStartMode.detached);
return true;
} catch (e) {
appLogger.w('Failed to launch $appName via open -a', error: e);
return false;
}
}
Future<bool> _launchCommand(String command, String url) async {
try {
final result = await Process.start(command, [url], mode: ProcessStartMode.detached);
appLogger.d('Launched $command with PID: ${result.pid}');
return true;
} catch (e) {
appLogger.w('Failed to launch $command', error: e);
return false;
}
}
Future<bool> _launchCustom(String value, String url, CustomPlayerType type) async {
if (type == CustomPlayerType.urlScheme) {
return _launchUrlScheme(value, url);
}
// Command type
if (Platform.isAndroid) {
return _launchAndroidIntent(url, package: value);
} else if (Platform.isMacOS) {
// Try PATH first (e.g. mpv), fall back to open -a (e.g. VLC)
if (await _launchCommand(value, url)) return true;
return _launchMacApp(value, url);
} else {
return _launchCommand(value, url);
}
}
// --- Known Players ---
class KnownPlayers {
static final systemDefault = ExternalPlayer(id: 'system_default', name: 'System Default', launch: _launchWithUrl);
static final _allPlayers = <ExternalPlayer>[
systemDefault,
ExternalPlayer(
id: 'vlc',
name: 'VLC',
iconAsset: 'assets/player_icons/vlc.svg',
isAvailable: Platform.isAndroid || Platform.isIOS || Platform.isMacOS || Platform.isLinux || Platform.isWindows,
launch: (url) async {
if (Platform.isAndroid) return _launchAndroidIntent(url, package: 'org.videolan.vlc');
if (Platform.isIOS) return _launchUrlScheme('vlc://', url);
if (Platform.isMacOS) return _launchMacApp('VLC', url);
return _launchCommand('vlc', url);
},
),
ExternalPlayer(
id: 'mpv',
name: 'mpv',
iconAsset: 'assets/player_icons/mpv.svg',
isAvailable: Platform.isAndroid || Platform.isMacOS || Platform.isLinux || Platform.isWindows,
launch: (url) async {
if (Platform.isAndroid) return _launchAndroidIntent(url, package: 'is.xyz.mpv');
return _launchCommand('mpv', url);
},
),
ExternalPlayer(
id: 'iina',
name: 'IINA',
iconAsset: 'assets/player_icons/iina.png',
isAvailable: Platform.isMacOS,
launch: (url) => _launchUrlScheme('iina://weblink?url=', url),
),
ExternalPlayer(
id: 'mx_player',
name: 'MX Player',
iconAsset: 'assets/player_icons/mx_player.svg',
isAvailable: Platform.isAndroid,
launch: (url) => _launchAndroidIntent(url, package: 'com.mxtech.videoplayer.ad'),
),
ExternalPlayer(
id: 'just_player',
name: 'Just Player',
iconAsset: 'assets/player_icons/just_player.png',
isAvailable: Platform.isAndroid,
launch: (url) => _launchAndroidIntent(url, package: 'com.brouken.player'),
),
ExternalPlayer(
id: 'infuse',
name: 'Infuse',
iconAsset: 'assets/player_icons/infuse.png',
isAvailable: Platform.isIOS,
launch: (url) => _launchUrlScheme('infuse://x-callback-url/play?url=', url),
),
ExternalPlayer(
id: 'potplayer',
name: 'PotPlayer',
iconAsset: 'assets/player_icons/potplayer.png',
isAvailable: Platform.isWindows,
launch: (url) async {
if (await _launchUrlScheme('potplayer://', url)) return true;
return _launchCommand('PotPlayerMini64', url);
},
),
ExternalPlayer(
id: 'celluloid',
name: 'Celluloid',
iconAsset: 'assets/player_icons/celluloid.svg',
isAvailable: Platform.isLinux,
launch: (url) => _launchCommand('celluloid', url),
),
];
/// Get players available on the current platform
static List<ExternalPlayer> getForCurrentPlatform() {
return _allPlayers.where((p) => p.isAvailable).toList();
}
/// Find a known player by ID
static ExternalPlayer? findById(String id) {
try {
return _allPlayers.firstWhere((p) => p.id == id);
} catch (_) {
return null;
}
}
}
@@ -0,0 +1,265 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:plezy/widgets/app_icon.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../../i18n/strings.g.dart';
import '../../models/external_player_models.dart';
import '../../services/settings_service.dart';
import '../../widgets/desktop_app_bar.dart';
class ExternalPlayerScreen extends StatefulWidget {
const ExternalPlayerScreen({super.key});
@override
State<ExternalPlayerScreen> createState() => _ExternalPlayerScreenState();
}
class _ExternalPlayerScreenState extends State<ExternalPlayerScreen> {
late SettingsService _settingsService;
bool _isLoading = true;
bool _useExternalPlayer = false;
ExternalPlayer _selectedPlayer = KnownPlayers.systemDefault;
List<ExternalPlayer> _customPlayers = [];
@override
void initState() {
super.initState();
_loadSettings();
}
Future<void> _loadSettings() async {
_settingsService = await SettingsService.getInstance();
setState(() {
_useExternalPlayer = _settingsService.getUseExternalPlayer();
_selectedPlayer = _settingsService.getSelectedExternalPlayer();
_customPlayers = _settingsService.getCustomExternalPlayers();
_isLoading = false;
});
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return const Scaffold(body: Center(child: CircularProgressIndicator()));
}
final knownPlayers = KnownPlayers.getForCurrentPlatform();
return Scaffold(
body: CustomScrollView(
slivers: [
CustomAppBar(title: Text(t.externalPlayer.title), pinned: true),
SliverPadding(
padding: const EdgeInsets.all(16),
sliver: SliverList(
delegate: SliverChildListDelegate([
Card(
child: SwitchListTile(
secondary: const AppIcon(Symbols.open_in_new_rounded, fill: 1),
title: Text(t.externalPlayer.useExternalPlayer),
subtitle: Text(t.externalPlayer.useExternalPlayerDescription),
value: _useExternalPlayer,
onChanged: (value) async {
setState(() => _useExternalPlayer = value);
await _settingsService.setUseExternalPlayer(value);
},
),
),
if (_useExternalPlayer) ...[
const SizedBox(height: 16),
Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Text(
t.externalPlayer.selectPlayer,
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
),
),
// Known players
...knownPlayers.map((player) => _buildPlayerTile(player)),
// Custom players
if (_customPlayers.isNotEmpty) const Divider(),
..._customPlayers.map((player) => _buildPlayerTile(player, isCustom: true)),
// Add custom player button
const Divider(),
ListTile(
leading: const AppIcon(Symbols.add_rounded, fill: 1),
title: Text(t.externalPlayer.addCustomPlayer),
onTap: _showAddCustomPlayerDialog,
),
],
),
),
],
]),
),
),
],
),
);
}
Widget _buildPlayerTile(ExternalPlayer player, {bool isCustom = false}) {
final isSelected = _selectedPlayer.id == player.id;
Widget leading;
if (player.iconAsset != null) {
leading = ClipRRect(
borderRadius: BorderRadius.circular(6),
child: player.iconAsset!.endsWith('.svg')
? SvgPicture.asset(player.iconAsset!, width: 32, height: 32)
: Image.asset(player.iconAsset!, width: 32, height: 32, errorBuilder: (_, __, ___) {
return const AppIcon(Symbols.play_circle_rounded, fill: 1, size: 32);
}),
);
} else if (player.id == 'system_default') {
leading = const AppIcon(Symbols.open_in_new_rounded, fill: 1, size: 32);
} else {
leading = const AppIcon(Symbols.play_circle_rounded, fill: 1, size: 32);
}
return ListTile(
leading: leading,
title: Text(player.id == 'system_default' ? t.externalPlayer.systemDefault : player.name),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (isCustom)
IconButton(
icon: const AppIcon(Symbols.delete_rounded, fill: 1, size: 20),
onPressed: () => _deleteCustomPlayer(player),
),
AppIcon(
isSelected ? Symbols.radio_button_checked_rounded : Symbols.radio_button_unchecked_rounded,
fill: 1,
color: isSelected ? Theme.of(context).colorScheme.primary : null,
),
],
),
onTap: () async {
setState(() => _selectedPlayer = player);
await _settingsService.setSelectedExternalPlayer(player);
},
);
}
Future<void> _deleteCustomPlayer(ExternalPlayer player) async {
await _settingsService.removeCustomExternalPlayer(player.id);
setState(() {
_customPlayers.removeWhere((p) => p.id == player.id);
_selectedPlayer = _settingsService.getSelectedExternalPlayer();
});
}
Future<void> _showAddCustomPlayerDialog() async {
final nameController = TextEditingController();
final valueController = TextEditingController();
var selectedType = CustomPlayerType.command;
final result = await showDialog<bool>(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setDialogState) {
final isUrlScheme = selectedType == CustomPlayerType.urlScheme;
final String fieldLabel;
final String fieldHint;
if (isUrlScheme) {
fieldLabel = t.externalPlayer.playerUrlScheme;
fieldHint = 'myplayer://play?url=';
} else if (Platform.isAndroid) {
fieldLabel = t.externalPlayer.playerPackage;
fieldHint = 'com.example.player';
} else {
fieldLabel = t.externalPlayer.playerCommand;
fieldHint = Platform.isMacOS ? 'mpv' : '/usr/bin/player';
}
return AlertDialog(
title: Text(t.externalPlayer.addCustomPlayer),
content: SizedBox(
width: 300,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: nameController,
decoration: InputDecoration(
labelText: t.externalPlayer.playerName,
hintText: 'My Player',
),
autofocus: true,
textInputAction: TextInputAction.next,
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: SegmentedButton<CustomPlayerType>(
segments: [
ButtonSegment(
value: CustomPlayerType.command,
label: Text(Platform.isAndroid
? t.externalPlayer.playerPackage
: t.externalPlayer.playerCommand),
),
ButtonSegment(
value: CustomPlayerType.urlScheme,
label: Text(t.externalPlayer.playerUrlScheme),
),
],
selected: {selectedType},
onSelectionChanged: (value) {
setDialogState(() => selectedType = value.first);
},
),
),
const SizedBox(height: 16),
TextField(
controller: valueController,
decoration: InputDecoration(
labelText: fieldLabel,
hintText: fieldHint,
),
textInputAction: TextInputAction.done,
),
],
),
),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: Text(t.common.cancel)),
FilledButton(
onPressed: () {
if (nameController.text.isNotEmpty && valueController.text.isNotEmpty) {
Navigator.pop(context, true);
}
},
child: Text(t.common.save),
),
],
);
},
),
);
if (result != true) return;
final id = 'custom_${DateTime.now().millisecondsSinceEpoch}';
final newPlayer = ExternalPlayer.custom(
id: id,
name: nameController.text,
value: valueController.text,
type: selectedType,
);
await _settingsService.addCustomExternalPlayer(newPlayer);
setState(() {
_customPlayers.add(newPlayer);
});
}
}
+22
View File
@@ -31,6 +31,7 @@ import '../../providers/companion_remote_provider.dart';
import '../../screens/companion_remote/mobile_remote_screen.dart';
import '../../widgets/companion_remote/remote_session_dialog.dart';
import 'about_screen.dart';
import 'external_player_screen.dart';
import 'logs_screen.dart';
import 'mpv_config_screen.dart';
import 'subtitle_styling_screen.dart';
@@ -68,6 +69,7 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab {
static const _kShowUnwatchedCount = 'show_unwatched_count';
static const _kRequireProfileSelectionOnOpen = 'require_profile_selection_on_open';
static const _kPlayerBackend = 'player_backend';
static const _kExternalPlayer = 'external_player';
static const _kHardwareDecoding = 'hardware_decoding';
static const _kMatchContentFrameRate = 'match_content_frame_rate';
static const _kBufferSize = 'buffer_size';
@@ -115,6 +117,8 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab {
bool _matchContentFrameRate = false;
bool _useExoPlayer = true; // Android only: ExoPlayer vs MPV
bool _requireProfileSelectionOnOpen = false;
bool _useExternalPlayer = false;
String _selectedExternalPlayerName = '';
// Update checking state
bool _isCheckingForUpdate = false;
@@ -182,6 +186,8 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab {
_matchContentFrameRate = _settingsService.getMatchContentFrameRate();
_useExoPlayer = _settingsService.getUseExoPlayer();
_requireProfileSelectionOnOpen = _settingsService.getRequireProfileSelectionOnOpen();
_useExternalPlayer = _settingsService.getUseExternalPlayer();
_selectedExternalPlayerName = _settingsService.getSelectedExternalPlayer().name;
_isLoading = false;
});
}
@@ -410,6 +416,22 @@ class _SettingsScreenState extends State<SettingsScreen> with FocusableTab {
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
onTap: () => _showPlayerBackendDialog(),
),
ListTile(
focusNode: _focusTracker.get(_kExternalPlayer),
leading: const AppIcon(Symbols.open_in_new_rounded, fill: 1),
title: Text(t.externalPlayer.title),
subtitle: Text(_useExternalPlayer ? _selectedExternalPlayerName : t.externalPlayer.off),
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
onTap: () async {
await Navigator.push(context, MaterialPageRoute(builder: (context) => const ExternalPlayerScreen()));
// Reload to reflect any changes
final s = await settings.SettingsService.getInstance();
setState(() {
_useExternalPlayer = s.getUseExternalPlayer();
_selectedExternalPlayerName = s.getSelectedExternalPlayer().name;
});
},
),
SwitchListTile(
focusNode: _focusTracker.get(_kHardwareDecoding),
secondary: const AppIcon(Symbols.hardware_rounded, fill: 1),
+50
View File
@@ -0,0 +1,50 @@
import 'package:flutter/material.dart';
import '../models/external_player_models.dart';
import '../models/plex_metadata.dart';
import '../utils/app_logger.dart';
import '../utils/snackbar_helper.dart';
import '../i18n/strings.g.dart';
import 'plex_client.dart';
import 'settings_service.dart';
class ExternalPlayerService {
static Future<bool> launch({
required BuildContext context,
required PlexMetadata metadata,
required PlexClient client,
int mediaIndex = 0,
}) async {
try {
final playbackData = await client.getVideoPlaybackData(
metadata.ratingKey,
mediaIndex: mediaIndex,
);
if (!playbackData.hasValidVideoUrl) {
if (context.mounted) {
showErrorSnackBar(context, t.messages.fileInfoNotAvailable);
}
return false;
}
final videoUrl = playbackData.videoUrl!;
final settings = await SettingsService.getInstance();
final player = settings.getSelectedExternalPlayer();
appLogger.d('Launching external player: ${player.name} with URL: $videoUrl');
final launched = await player.launch(videoUrl);
if (!launched && context.mounted) {
showErrorSnackBar(context, t.externalPlayer.appNotInstalled(name: player.name));
}
return launched;
} catch (e) {
appLogger.e('Failed to launch external player', error: e);
if (context.mounted) {
showErrorSnackBar(context, t.externalPlayer.launchFailed);
}
return false;
}
}
}
+62
View File
@@ -4,6 +4,7 @@ import '../models/hotkey_model.dart';
import 'package:plezy/utils/app_logger.dart';
import '../i18n/strings.g.dart';
import '../models/mpv_config_models.dart';
import '../models/external_player_models.dart';
import 'base_shared_preferences_service.dart';
import '../utils/platform_detector.dart';
@@ -70,6 +71,9 @@ class SettingsService extends BaseSharedPreferencesService {
static const String _keyShowUnwatchedCount = 'show_unwatched_count';
static const String _keyGlobalShaderPreset = 'global_shader_preset';
static const String _keyRequireProfileSelectionOnOpen = 'require_profile_selection_on_open';
static const String _keyUseExternalPlayer = 'use_external_player';
static const String _keySelectedExternalPlayer = 'selected_external_player';
static const String _keyCustomExternalPlayers = 'custom_external_players';
SettingsService._();
@@ -1048,6 +1052,61 @@ class SettingsService extends BaseSharedPreferencesService {
return prefs.getBool(_keyRequireProfileSelectionOnOpen) ?? false;
}
// External Player
Future<void> setUseExternalPlayer(bool enabled) async {
await prefs.setBool(_keyUseExternalPlayer, enabled);
}
bool getUseExternalPlayer() {
return prefs.getBool(_keyUseExternalPlayer) ?? false;
}
Future<void> setSelectedExternalPlayer(ExternalPlayer player) async {
await prefs.setString(_keySelectedExternalPlayer, player.toJsonString());
}
ExternalPlayer getSelectedExternalPlayer() {
final jsonString = prefs.getString(_keySelectedExternalPlayer);
if (jsonString == null) return KnownPlayers.systemDefault;
try {
return ExternalPlayer.fromJsonString(jsonString);
} catch (_) {
return KnownPlayers.systemDefault;
}
}
Future<void> setCustomExternalPlayers(List<ExternalPlayer> players) async {
final jsonString = json.encode(players.map((p) => p.toJson()).toList());
await prefs.setString(_keyCustomExternalPlayers, jsonString);
}
List<ExternalPlayer> getCustomExternalPlayers() {
final jsonString = prefs.getString(_keyCustomExternalPlayers);
if (jsonString == null) return [];
try {
final List<dynamic> decoded = json.decode(jsonString);
return decoded.map((e) => ExternalPlayer.fromJson(e as Map<String, dynamic>)).toList();
} catch (_) {
return [];
}
}
Future<void> addCustomExternalPlayer(ExternalPlayer player) async {
final players = getCustomExternalPlayers();
players.add(player);
await setCustomExternalPlayers(players);
}
Future<void> removeCustomExternalPlayer(String id) async {
final players = getCustomExternalPlayers();
players.removeWhere((p) => p.id == id);
await setCustomExternalPlayers(players);
// If the removed player was selected, reset to system default
if (getSelectedExternalPlayer().id == id) {
await setSelectedExternalPlayer(KnownPlayers.systemDefault);
}
}
// Reset all settings to defaults
Future<void> resetAllSettings() async {
await Future.wait([
@@ -1098,6 +1157,9 @@ class SettingsService extends BaseSharedPreferencesService {
prefs.remove(_keyShowUnwatchedCount),
prefs.remove(_keyGlobalShaderPreset),
prefs.remove(_keyRequireProfileSelectionOnOpen),
prefs.remove(_keyUseExternalPlayer),
prefs.remove(_keySelectedExternalPlayer),
prefs.remove(_keyCustomExternalPlayers),
]);
}
+22
View File
@@ -3,7 +3,9 @@ import 'package:flutter/material.dart';
import '../mpv/mpv.dart';
import '../models/plex_metadata.dart';
import '../screens/video_player_screen.dart';
import '../services/external_player_service.dart';
import '../services/settings_service.dart';
import '../utils/provider_extensions.dart';
import 'app_logger.dart';
const String kVideoPlayerRouteName = '/video_player';
@@ -54,6 +56,26 @@ Future<bool?> navigateToVideoPlayer(
}
}
// Check if external player is enabled (skip for offline mode)
if (!isOffline) {
try {
final settingsService = await SettingsService.getInstance();
if (settingsService.getUseExternalPlayer()) {
final client = context.getClientForMetadata(metadata);
final launched = await ExternalPlayerService.launch(
context: context,
metadata: metadata,
client: client,
mediaIndex: mediaIndex,
);
if (launched) return null;
// Fall through to built-in player on failure
}
} catch (e) {
appLogger.w('External player launch failed, falling back to built-in player', error: e);
}
}
// Prevent stacking an identical video player when already active
if (!usePushReplacement &&
VideoPlayerScreenState.activeRatingKey == metadata.ratingKey &&
+28
View File
@@ -16,6 +16,7 @@ import '../utils/library_refresh_notifier.dart';
import '../utils/snackbar_helper.dart';
import '../utils/dialogs.dart';
import '../utils/focus_utils.dart';
import '../services/external_player_service.dart';
import '../focus/dpad_navigator.dart';
import '../screens/media_detail_screen.dart';
import '../screens/season_detail_screen.dart';
@@ -217,6 +218,17 @@ class MediaContextMenuState extends State<MediaContextMenu> {
menuActions.add(_MenuAction(value: 'fileinfo', icon: Symbols.info_rounded, label: t.mediaMenu.fileInfo));
}
// Play in External Player (for episodes and movies)
if (mediaType == PlexMediaType.episode || mediaType == PlexMediaType.movie) {
menuActions.add(
_MenuAction(
value: 'play_external',
icon: Symbols.open_in_new_rounded,
label: t.externalPlayer.playInExternalPlayer,
),
);
}
// Download options (for episodes, movies, shows, and seasons)
if (mediaType == PlexMediaType.episode ||
mediaType == PlexMediaType.movie ||
@@ -417,6 +429,10 @@ class MediaContextMenuState extends State<MediaContextMenu> {
await _handleDelete(context, isCollection, isPlaylist);
break;
case 'play_external':
await _handlePlayExternal(context);
break;
case 'download':
await _handleDownload(context);
break;
@@ -981,6 +997,18 @@ class MediaContextMenuState extends State<MediaContextMenu> {
}
}
/// Handle play in external player action
Future<void> _handlePlayExternal(BuildContext context) async {
final metadata = widget.item as PlexMetadata;
final client = _getClientForItem();
await ExternalPlayerService.launch(
context: context,
metadata: metadata,
client: client,
);
}
/// Handle download action
Future<void> _handleDownload(BuildContext context) async {
final downloadProvider = Provider.of<DownloadProvider>(context, listen: false);
+8
View File
@@ -25,6 +25,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.13.4"
android_intent_plus:
dependency: "direct main"
description:
name: android_intent_plus
sha256: "2329378af63f49b985cb2e110ac784d08374f1e2b1984be77ba9325b1c8cce11"
url: "https://pub.dev"
source: hosted
version: "5.3.1"
ansicolor:
dependency: transitive
description:
+2
View File
@@ -52,6 +52,7 @@ dependencies:
dart_discord_presence: ^1.1.0
flutter_svg: ^2.2.3
mobile_scanner: ^6.0.2
android_intent_plus: ^5.0.2
dev_dependencies:
flutter_test:
@@ -72,6 +73,7 @@ flutter:
- assets/go-noto-current-regular.ttf
- assets/shaders/nvscaler/
- assets/shaders/anime4k/
- assets/player_icons/
- assets/rating_icons/
flutter_launcher_icons: