Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: linux support #251

Merged
merged 17 commits into from
Apr 16, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions .github/workflows/prbuild.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
- name: Flutter action
uses: subosito/flutter-action@v2
with:
flutter-version: 3.16.8
flutter-version: 3.16.8
channel: stable
- name: Decode keystore
run: |
Expand Down Expand Up @@ -64,7 +64,7 @@ jobs:
- uses: actions/checkout@v3
- uses: subosito/[email protected]
with:
flutter-version: 3.16.8
flutter-version: 3.16.8
channel: stable
- name: Install project dependencies
run: flutter pub get
Expand All @@ -79,3 +79,27 @@ jobs:
with:
path: "build/windows/x64/runner/Miru-App"
name: Miru-pr-${{ github.event.pull_request.number }}-windows.zip

build-and-release-linux:
runs-on: ubuntu-latest
steps:
- name: Install Dependencies
run: sudo apt-get install ninja-build build-essential libgtk-3-dev libmpv-dev mpv
- uses: actions/checkout@v3
- uses: subosito/[email protected]
with:
flutter-version: 3.16.8
channel: stable
- name: Install project dependencies
run: flutter pub get
- name: Build artifacts
run: flutter build linux --release
- name: Rename Release Directory Name to Miru-App # 为了解压缩后更好看一点
run: |
mv build/linux/x64/release/bundle build/linux/x64/release/Miru-App
# 发布安装包
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
path: "build/linux/x64/release/Miru-App"
name: Miru-pr-${{ github.event.pull_request.number }}-linux.zip
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ migrate_working_dir/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
.vscode/

# Flutter/Dart/Pub related
**/doc/api/
Expand Down
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"cmake.sourceDirectory": "/home/noonecare/Documents/GitHub/miru-app/linux"
}
13 changes: 13 additions & 0 deletions lib/.vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Dart & Flutter",
"request": "launch",
"type": "dart"
}
]
}
3 changes: 2 additions & 1 deletion lib/controllers/detail_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,8 @@ class DetailPageController extends GetxController {
await launchMobileExternalPlayer(watchData.url, player);
return;
}
await launchDesktopExternalPlayer(watchData.url, player);
await launchDesktopExternalPlayer(watchData.url, player,
watchData.headers ?? {}, watchData.subtitles ?? []);
return;
} catch (e) {
showPlatformSnackbar(
Expand Down
170 changes: 170 additions & 0 deletions lib/data/services/extension.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
class Element {
constructor(content, selector) {
this.content = content;
this.selector = selector || "";
}
async querySelector(selector) {
return new Element(await this.execute(), selector);
}

async execute(fun) {
await DartBridge.sendMessage("querySelector", JSON.stringify([this.content, this.selector, fun]));
}

async removeSelector(selector) {
this.content = await sendMessage(
"removeSelector",
JSON.stringify([await this.outerHTML, selector])
);
return this;
}

async getAttributeText(attr) {
return await sendMessage(
"getAttributeText",
JSON.stringify([await this.outerHTML, this.selector, attr])
);
}

get text() {
return this.execute("text");
}

get outerHTML() {
return this.execute("outerHTML");
}

get innerHTML() {
return this.execute("innerHTML");
}
}
class XPathNode {
constructor(content, selector) {
this.content = content;
this.selector = selector;
}

async excute(fun) {
return await sendMessage(
"queryXPath",
JSON.stringify([this.content, this.selector, fun])
);
}

get attr() {
return this.excute("attr");
}

get attrs() {
return this.excute("attrs");
}

get text() {
return this.excute("text");
}

get allHTML() {
return this.excute("allHTML");
}

get outerHTML() {
return this.excute("outerHTML");
}
}

// 重写 console.log
console.log = function (message) {
if (typeof message === "object") {
message = JSON.stringify(message);
}
DartBridge.sendMessage("log$className", JSON.stringify([message.toString()]));
};
class Extension {
package = "${extension.package}";
name = "${extension.name}";
// 在 load 中注册的 keys
settingKeys = [];

querySelector(content, selector) {
return new Element(content, selector);
}
async request(url, options) {
options = options || {};
options.headers = options.headers || {};
const miruUrl = options.headers["Miru-Url"] || "${extension.webSite}";
options.method = options.method || "get";
var message = null
const waitForChange = new Promise(resolve => {
DartBridge.setHandler("request$className", async (res) => {
try {
message = JSON.parse(res);
} catch (e) {
message = res;
}
resolve();
});
});

DartBridge.sendMessage("request$className", JSON.stringify([miruUrl + url, options, "${extension.package}"]));
await waitForChange;
console.log("Dart Bridge Passed");
return message;
}
queryXPath(content, selector) {
return new XPathNode(content, selector);
}
async querySelectorAll(content, selector) {
const elements = [];
const waitForChange = new Promise(resolve => {
DartBridge.setHandler("querySelectorAll", async (arg) => {

const message = JSON.parse(arg);
for (const e of message) {
elements.push(new Element(e, selector));
}
resolve();
})
});
DartBridge.sendMessage("querySelectorAll$className", JSON.stringify([content, selector]));
await waitForChange;
return elements;
}
async getAttributeText(content, selector, attr) {
return await sendMessage(
"getAttributeText",
JSON.stringify([content, selector, attr])
);
}
latest(page) {
throw new Error("not implement latest");
}
search(kw, page, filter) {
throw new Error("not implement search");
}
createFilter(filter) {
throw new Error("not implement createFilter");
}
detail(url) {
throw new Error("not implement detail");
}
watch(url) {
throw new Error("not implement watch");
}
checkUpdate(url) {
throw new Error("not implement checkUpdate");
}
async getSetting(key) {
return sendMessage("getSetting", JSON.stringify([key]));
}
async registerSetting(settings) {
console.log(JSON.stringify([settings]));
this.settingKeys.push(settings.key);
return sendMessage("registerSetting", JSON.stringify([settings]));
}
async load() { }
}

async function stringify(callback) {
const data = await callback();
return typeof data === "object" ? JSON.stringify(data, 0, 2) : data;
}
136 changes: 136 additions & 0 deletions lib/data/services/extension_jscore_plugin.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import 'dart:async';
import 'dart:convert';

import 'package:flutter_js/flutter_js.dart';
import 'package:miru_app/utils/log.dart';

const DART_BRIDGE_MESSAGE_NAME = 'DART_BRIDGE_MESSAGE_NAME';

class JsBridge {
final JavascriptRuntime jsRuntime;
int _messageCounter = 0;
static final Map<int, Completer> _pendingRequests = {};
final Object? Function(Object? value)? toEncodable;
static final Map<String, Future<dynamic> Function(dynamic message)>
_handlers = {};
JsBridge({
required this.jsRuntime,
this.toEncodable,
}) {
final bridgeScriptEvalResult = jsRuntime.evaluate(JS_BRIDGE_JS);
if (bridgeScriptEvalResult.isError) {
logger.info('Error eval bridge script');
}
final windowEvalResult =
jsRuntime.evaluate('var window = global = globalThis;');
if (windowEvalResult.isError) {
logger.info('Error eval window script');
}
jsRuntime.onMessage(DART_BRIDGE_MESSAGE_NAME, (message) {
_onMessage(message);
});
}

_onMessage(dynamic message) async {
if (message['isRequest']) {
final handler = _handlers[message['name']];
if (handler == null) {
logger.info('Error: no handlers for message $message');
} else {
final result = await handler(message['args']);
final jsResult = jsRuntime.evaluate(
'onMessageFromDart(false, ${message['callId']}, "${message['name']}",${jsonEncode(result, toEncodable: toEncodable)})');
if (jsResult.isError) {
logger.info('Error sending message to JS: $jsResult');
}
}
} else {
final completer = _pendingRequests.remove(message['callId']);
if (completer == null) {
logger.info('Error: no completer for response for message $message');
} else {
completer.complete(message['result']);
}
}
}

sendMessage(String name, dynamic message) async {
if (_messageCounter > 999999999) {
_messageCounter = 0;
}
_messageCounter += 1;
final completer = Completer();
_pendingRequests[_messageCounter] = completer;
final jsResult = jsRuntime.evaluate(
'window.onMessageFromDart(true, $_messageCounter, "$name",${jsonEncode(message, toEncodable: toEncodable)})');
if (jsResult.isError) {
logger.info('Error sending message to JS: $jsResult');
}

return completer.future;
}

// final _handlers = {};

setHandler(String name, Future<dynamic> Function(dynamic message) handler) {
_handlers[name] = handler;
}
}

const JS_BRIDGE_JS = '''
globalThis.DartBridge = (() => {
let callId = 0;
const DART_BRIDGE_MESSAGE_NAME = '$DART_BRIDGE_MESSAGE_NAME';
globalThis.onMessageFromDart = async (isRequest, callId, name, args) => {
if (isRequest) {
if (handlers[name]) {
sendMessage(DART_BRIDGE_MESSAGE_NAME, JSON.stringify({
isRequest: false,
callId,
name,
result: await handlers[name](args),
}));
}
}
else {
const pendingResolve = pendingRequests[callId];
delete pendingRequests[callId];
if (pendingResolve) {
pendingResolve(args);
}
}
return null;
};
const handlers = {};
const pendingRequests = {};
return {
sendMessage: async (name, args) => {
if (callId > 999999999) {
callId = 0;
}
callId += 1;
sendMessage(DART_BRIDGE_MESSAGE_NAME, JSON.stringify({
isRequest: true,
callId,
name,
args,
}),call=((res)=>{}));
return new Promise((resolve) => {
pendingRequests[callId] = resolve;
call(resolve)
});
},
setHandler: (name, handler) => {
handlers[name] = handler;
},
resolveRequest: (callId, result) => {
sendMessage(DART_BRIDGE_MESSAGE_NAME, JSON.stringify({
isRequest: false,
callId,
result,
}));
},
};
})();
global = globalThis;
''';
Loading
Loading