2024年11月15日 星期五

github actions使用進化,使用runner來佈版吧

我就覺得奇怪,最早喬納斯推薦我這套,我就在質疑,一開始跑jenkins是跑在server端,這樣才能對機器進行操作,github跑在遠端是要怎麼進行佈版啦。

一開始也沒什麼概念,於是我突發奇想,使用ssh連線,使用sftp上傳檔案,或是使用nodejs腳本

一直到最近遇到windows open ssh不穩的問題,如果短時間遇到一個帳號同時登入,ssh就會掛掉的狀況,我只好另求出路

原本想說要使用drone來作為cicd工具,看到要透過docker啟動兩個服務,一個是server一個是runner,我對這個也沒有很了解所以看過就算了,但server我有點不太想亂裝東西,所以還是放後面吧

在多方查找之下才發現,actions有各式各樣的套件可以與不同的服務進行部署,azure, aws之類的,我想想會不會有iis的pipeline可以使用,結果是有的,但使用上好像會失敗,有興趣的大大可以自己玩一下

再來我逛到保哥的網誌,裡面有用到一個可以上傳跟下載的套件(actions/upload-artifact,actions/download-artifact),接著再看到這篇提問,裡面幾個關鍵字,needs: build,run-on: self_hosted,needs我看得懂,就是可以把任務串連起來(沒串連之前測試都是併發)那self_hosted難道是可以跑在本機上?接著我就開始研究這些關鍵字

原來有github有個服務叫github runner,可以在伺服器跑起來,這樣run-on: self_hosted就可以在之後step寫要做的動作了,這招妙啊!原來runner就是監聽器的服務啊~(drone也有)

要在伺服器把github runner跑起來,可以參考這篇教學,可以選擇伺服器的作業系統進行安裝與啟用,基本上從第一行執行到最後一行就可以完成了

這邊記錄下我遇到的兩個問題

第一行下載zip包,遇到ssltls安全問題,可以把網址複製到chrome上直接下載

最後一個./run.cmd,這行跑起來沒問題,服務也持續啟動,但問題是到時腳本跑在機器上會在github-runner資料夾下面創run.cmd資料夾,到時會有檔名衝突問題,所以當啟動完就要把檔名改掉,這超不合理的啊,我也不知對什麼會這樣,可能是我設定哪裡出問題了。

圖中我把run.cmd改成run_service.cmd了,另外成功的跑了一些程序


接著就可以開始撰寫腳本了

name: 81-dev

on:
push:
branches:
- "dev"

env:
PROJECT_NAME: "81 dev"
PROJECT_FOLDER: "81c"
TELEGRAM_TOKEN: "tg_token"
TELEGRAM_CHAT_ID: "chat_id"

jobs:
build:
runs-on: ubuntu-latest # runs-on字段指定运行所需要的虚拟机环境。注意:这个是必填字段
steps:
- uses: actions/checkout@v3
- name: Setup Node 14
uses: actions/setup-node@v3.5.1
with:
node-version: "14"
cache: "npm"

- name: Install yarn
run: |
npm i -g yarn

- name: build
run: |
yarn
yarn build

- name: Upload a Build Artifact
uses: actions/upload-artifact@v4
with:
name: application
path: dist/**/*
deploy:
needs: build
runs-on: self-hosted

steps:
- name: remove old files
run: |
Remove-Item -Path D:\wwwroot\${{ env.PROJECT_FOLDER }}\static\* -Recurse -Force
Remove-Item -Path D:\wwwroot\${{ env.PROJECT_FOLDER }}\index.html -Force

- name: Download new binaries over the top of the app
uses: actions/download-artifact@v4
with:
name: application
path: D:\wwwroot\${{ env.PROJECT_FOLDER }}

notify:
needs: deploy
runs-on: ubuntu-latest
steps:
- name: telegram notify
run: |
curl -d '{"method": "sendMessage", "chat_id": "${{ env.TELEGRAM_CHAT_ID }}", "text": "${{ env.PROJECT_NAME }} deployed."}' -H "Content-Type: application/json" -X POST https://api.telegram.org/bot${{ env.TELEGRAM_TOKEN }}/



小結
這專案打包下來2.6mb,這樣扣除build跟上傳時間,僅算下載及佈版,時間都落在30秒內,算是非常快速跟滿意。
最終還是有個小問題,如果是在organization創建的runner那這self_hosted都會指向同一台,沒辦法多台或命名嗎?這之後再來研究吧


補充

上線問claude他這樣回答我

self-hosted runner 可以自訂命名和設定群組:

  1. 在 GitHub Actions 工作流程中指定特定的 runner:
  1. 設定 runner 時的命名和標籤:
# 基本設定命令格式 ./config.cmd --url https://github.com/{YOUR_USERNAME}/{YOUR_REPO} --token {YOUR_TOKEN} --name "MY-IIS-RUNNER" --labels prod-iis,windows,vue # 例如: ./config.cmd --url https://github.com/your-name/your-repo --token abcd1234 --name "PROD-IIS-01" --labels prod-iis,windows,vue

參數說明:

  • --name: runner 的名稱
  • --labels: runner 的標籤,可以設定多個,用逗號分隔
  • --work: 指定工作目錄 (預設是 _work)
  • --group: 指定 runner 群組
  1. 如果要修改現有的 runner:
# 1. 先移除現有的 runner 設定 ./config.cmd remove # 2. 重新設定 ./config.cmd --url https://github.com/{YOUR_USERNAME}/{YOUR_REPO} --token {YOUR_TOKEN} --name "NEW-NAME" --labels new-label-1,new-label-2
  1. 設定多環境範例:
# 正式環境 runner ./config.cmd --name "PROD-IIS-01" --labels prod-iis,windows,vue # 測試環境 runner ./config.cmd --name "TEST-IIS-01" --labels test-iis,windows,vue # 開發環境 runner ./config.cmd --name "DEV-IIS-01" --labels dev-iis,windows,vue

然後在工作流程中可以這樣使用:

jobs: deploy-prod: runs-on: [self-hosted, prod-iis] if: github.ref == 'refs/heads/main' # ... deploy-test: runs-on: [self-hosted, test-iis] if: github.ref == 'refs/heads/develop' # ... deploy-dev: runs-on: [self-hosted, dev-iis] if: github.ref == 'refs/heads/feature/*' # ...

有機會再來試吧


2024年11月6日 星期三

業界鬼故事第一彈

不知不覺已經四個月沒發文了,看標題就知道這次不是HD係列,只是小編走在路上不小心聽到的。

原本相安無事日子過得也是好不自在,偏偏小編最近家中換車,需要一大筆資金,剛好小編工作沒有勞健保,所以只能付全額,想起來就覺得可怕。所以只好出來多打一份工作。

正愁沒有案源,小編想到了一直以來的按摩師朋友,向來他還滿欣賞自己能力,最近又剛創業(!?),應該會有系統開發需求,所以馬上預約了一堂課,準備來個反推銷。

這過程就不贅述,絕對沒有各位想聽的抓龍筋故事,想當然耳反推銷是失敗收場,不然哪來的鬼故事。

這朋友我姑且稱他為老K

老K:「我們店已經有找到人來開發系統了。」

我:「那也不錯啊,開發得如何?」

老K:「別說了,開發到現在四個月,我測一次錯一次,我要的功能也沒幾個,有個客人連三次使用都出錯,我到現在還是紙本作業」

我:「那幹嘛不換人(選我選我!)」

老K:「對方是老闆好朋友啊」

我:「那也只能祝你好運」

老K:「我跟你講有多扯,先講個最扯的,這個月初他把資料清空了,我問他有沒有備份,他說有的,然後他反問我有沒有紙本備份,我說都有系統了我用紙本幹嘛,他反倒怪我為什麼不用紙本備份!」

我:「......聲先奪人,高明!你老闆跟他真的是好朋友嗎?」

老K:「我們按摩師賣多少課,抽成這很正常吧,假設五成,1800課按摩師賺900,1400按摩師賺700,這應該很合理吧,上次開會老闆把我叫去罵,說你這樣系統會很難計算。我雖然不是學程式的,但不就是除以2而已嗎?這有很難嗎?」

我:「......他們真的是好朋友,我第一次聽到老闆幫工程師罵客人的!」

老K:「還有我們創建客戶理論上就照創建日期排列,他也是這樣告訴我的,結果我實際去看,我後來創建的排在中間,我說能不能照順序排,他竟然回答我不能,這要另外寫,要開發時間,正常不是要可以選擇用什麼排列嗎」

我:「我已經不知到要從哪裡吐槽了」

老K:「還有一個很扯,我們通常都使用掃QRCode扣課,必須在他的app介面才能掃,然後只能正著掃,橫著掃跟倒著掃都掃不出東西,如果用一般掃條碼的app掃會掃出客戶電話號碼」

因為大部分我的回答都是...所以就省略

老K:「還有他有一份excel可以計算薪水,只有客戶用qrcode掃課才能真的扣課,按摩師薪水才會計算上去,如果在後台手動扣課,excel不會更新,這樣數據肯定是錯的吧」

老K:「我們要去辦信用卡機,他還跟老闆打包票說他辦過絕對沒問題,最後來來回回送件了好幾次終於辦下來,結果一個客人刷卡完,我進後台看怎麼沒入帳,過一週後還是沒入帳,我就打給業務員,業務員開後台確認是有入帳的,最後確認到問題是他幫我申請的是線上刷卡的帳戶,不能在線下使用。那證明他根本就沒辦過嘛,好在他沒有把金流跟系統做整合」

老K:「他寫完的東西自己都沒在測試的,都直接交給我們,我也沒有特別刁難他,但真的是用一次錯一次」

老K:「而且你跟他講問題,他永遠不會正面回答你,例如上週他問我使用狀況,我列了五點給他,他只回我我再看看,你倒是告訴我你什麼時間點會修好什麼東西啊==」

我也只能一路聽他抱怨,我甚至有點可憐他想幫他重新寫一個,他過程一直問我這些功能如果是我要開發多久,我回答一週吧,真的是沒有很複雜的功能,只能說對方是我聽過的奇耙之一,這故事也再次證明了,寫程式很強真的沒什麼用,後台要夠硬才是硬道理!

最後大家如果同情老K可以到他店裡消費,按摩技術真的不錯,人在台中,有興趣可以留言給我

2024年7月24日 星期三

flutter第五彈,最後一哩路,v2ray專案,加上生命週期,遞迴bug修正,程式碼優化

聲明:本篇網誌沒有任何人被罵或受傷,請勿獵巫

正文開始

那個無限遞迴重整果然被開bug了,然後pm果然希望flutter背景執行要把vpn斷開,再來我也想把程式整理一下,我們開始以下故事吧

首先要監測app是否被移到背景執行需要有lifecycle,但flutter好像沒有相應pause的方法可以複寫,上網查好像也查不太到資料,幸好大神水哥又上演了神助攻

他推薦我一個plugin,看圖片就知道是他本人寫的,雖然沒上到官方hub,但以他技術應該是無虞,就直接git引用

# pubspec.yaml
dependencies:
...
mx_lifecycle:
git:
url: git@github.com:MagicalWater/mx_lifecycle.git

然後正常情況應該是會報 git@github.com: Permission denied

不要急,水哥繼續說,git套件安裝要走ssh,所以必須有ssh key,新增方法在這裡,產生pub key的指令就上網查,我就不贅述自己去看,完成就可以順利安裝完畢,至於使用範例,等等會在程式碼中

接著就上我們主菜吧

首先把V2ray連線拆成service

// lib/v2ray_service.dart
import 'package:flutter_v2ray/flutter_v2ray.dart';
import 'package:flutter/material.dart';

class V2rayService {
late final V2RayURL parser;
late final FlutterV2ray connection;
var status = ValueNotifier<V2RayStatus>(V2RayStatus());
var prevStatus = ValueNotifier<V2RayStatus>(V2RayStatus());
var hasReload = ValueNotifier<bool>(false);
bool get isConnected => status.value.state == "CONNECTED";

V2rayService(String connectText, Function updateView) {
parser = FlutterV2ray.parseFromURL(connectText);
connection = FlutterV2ray(
onStatusChanged: (_status) {
status.value = _status;
        // 觀察狀態變化
if (status.value != prevStatus.value) {
prevStatus.value = status.value;
          // 把可重整的值還原
hasReload.value = false;
}
updateView();
print("my status: ${status.value.state}");
},
);
}

init() {
return connection.initializeV2Ray();
}

connect() async {
if (await connection.requestPermission() && !isConnected) {
return connection.startV2Ray(
remark: parser.remark, config: parser.getFullConfiguration());
}
}

disconnect() {
if (isConnected) {
connection.stopV2Ray();
}
}

getVersion() {
return connection.getCoreVersion();
}
}

然後是主程式

// lib/main.dart
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:mx_lifecycle/mx_lifecycle.dart';
import "./v2ray_service.dart";

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter V2Ray',
theme: ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
inputDecorationTheme: const InputDecorationTheme(
border: OutlineInputBorder(),
),
),
home: SafeArea(
top: true,
right: true,
bottom: true,
left: true,
child: V2rayWebPage(),
),
);
}
}

class V2rayWebPage extends StatefulWidget {
const V2rayWebPage({super.key});

@override
State<V2rayWebPage> createState() => V2rayWebPageState();
}

class V2rayWebPageState extends State<V2rayWebPage> {
late final V2rayService v2rayService = new V2rayService(
"vmess://......",
() => setState(() {}));

late InAppWebViewController webViewController;
String url = "https://www.xxx.com/";

@override
void initState() {
super.initState();
v2rayService.init();
Future.delayed(const Duration(milliseconds: 500), () async {
print("version: ${await v2rayService.getVersion()}");
v2rayService.connect();
});
listenLifecycle();
}

// 水哥套件,lifecycle監聽
void listenLifecycle() {
final lifecycle = MxLifecycle();
lifecycle.lifecycleStateStream.listen((event) async {
if (defaultTargetPlatform == TargetPlatform.android) {
final state = lifecycle.toAndroid(event);
switch (state) {
case AndroidLifecycleState.paused:
v2rayService.disconnect();
            // 這裏暫存目前網址,以免到時重新連線又回到首頁 
url = (await webViewController.getUrl()).toString();
break;
case AndroidLifecycleState.started:
v2rayService.connect();
break;
default:
break;
}
print('android狀態: $state');
}
// else if (defaultTargetPlatform == TargetPlatform.iOS) {
// final state = lifecycle.toIos(event);
// print('ios狀態: $state');
// }
});
}

@override
Widget build(BuildContext context) {
return PopScope(
canPop: false,
onPopInvoked: (bool didPop) async {
if (await webViewController?.canGoBack() ?? false) {
webViewController?.goBack();
} else {
SystemNavigator.pop();
}
},
child: Scaffold(
body: !v2rayService.isConnected
? Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
ElevatedButton(
child: new Text("Connect"),
onPressed: () => v2rayService.connect(),
),
Container(height: 20.0), //SizedBox(height: 20.0),
],
)
: InAppWebView(
initialUrlRequest: URLRequest(url: WebUri(url)),
onWebViewCreated: (controller) {
webViewController = controller;
},
onReceivedError: (controller, req, error) {
                      // 這裡只給一次重整機會,防止遞迴
if (!v2rayService.hasReload.value) {
controller.reload();
v2rayService.hasReload.value = true;
print("req: ${req}");
print("error: ${error}");
}
},
onConsoleMessage: (controller, consoleMessage) {
print('Console message: ${consoleMessage.message}');
},
initialSettings: InAppWebViewSettings(
mixedContentMode:
MixedContentMode.MIXED_CONTENT_ALWAYS_ALLOW),
)));
}
}

最後也是最重要的步驟,就是要耐心的等,等到老闆沒耐心的時候,再把apk交出去,然後領績效獎金!!(咦!!

2024年7月23日 星期二

flutter第四彈,vpn旅途的終點?v2ray vpn連線

繼上一篇trojan-go後,這篇v2ray起步算是用飛的,至少flutter plugin有上官方hub,好笑的是,反而android連線v2ray好像沒有相關資訊

但...好像也沒這麼順利

我把程式碼寫好之後,一直無法正常觸發onStatusChanged(等等看到程式碼就知道我在說什麼了),連線也時好時壞,且無法掌握觸發時機

好在有trojan開發經驗,我學乖了,直接到官方github把release的source code載回家(code一樣沒有aar,唉~)

一開始在run也沒這麼順利,各種報錯,難道歷史要重演了嗎?

參考AndroidX MultiDex not found這篇,在example/android/app/build.gradle加上

dependencies {
implementation "androidx.multidex:multidex:2.0.1"
}

然後就run起來了,真是可喜可賀,接下來可以開始搬code了

// lib/man.dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:flutter_v2ray/flutter_v2ray.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter V2Ray',
theme: ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
inputDecorationTheme: const InputDecorationTheme(
border: OutlineInputBorder(),
),
),
home: const Scaffold(
body: HomePage(),
),
);
}
}

class HomePage extends StatefulWidget {
const HomePage({super.key});

@override
State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
late final FlutterV2ray flutterV2ray = FlutterV2ray(
onStatusChanged: (status) {
v2rayStatus.value = status;
setState(() {});
print("my status: ${v2rayStatus.value.state}");
},
);

final V2RayURL parser = FlutterV2ray.parseFromURL(
"vmess://......");

var v2rayStatus = ValueNotifier<V2RayStatus>(V2RayStatus()); // 類似signal可以監聽value

String? coreVersion;
bool get isVpnConnected => v2rayStatus.value.state == "CONNECTED"; // getter
late InAppWebViewController webViewController;

void connect() async {
if (await flutterV2ray.requestPermission()) {
flutterV2ray.startV2Ray(
remark: parser.remark, config: parser.getFullConfiguration());
}
}

@override
void initState() {
super.initState();
flutterV2ray.initializeV2Ray();
    // 類似setTimeout,可能是因為initialize還沒完成,所以需要延遲
    // 它異步沒寫好,就算放在await後也無法順利執行,只好這樣
Future.delayed(const Duration(milliseconds: 500), () async {
coreVersion = await flutterV2ray.getCoreVersion();
print("coreVersion: ${coreVersion}");
connect();
});
}

onBackClick(bool didPop) async {
if (await webViewController?.canGoBack() ?? false) {
webViewController?.goBack();
} else {
SystemNavigator.pop();
}
}

@override
Widget build(BuildContext context) {
return SafeArea(
top: true,
right: true,
bottom: true,
left: true,
child: PopScope(
canPop: false,
onPopInvoked: onBackClick,
child: Scaffold(
body: !isVpnConnected
? Column( // 置中文字
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
ElevatedButton(
child: new Text("Connect"),
onPressed: connect,
),
Container(height: 20.0), //SizedBox(height: 20.0),
],
)
: InAppWebView(
initialUrlRequest:
URLRequest(url: WebUri("https://www.xxx.com/")),
onWebViewCreated: (controller) {
webViewController = controller;
},
onReceivedError: (controller, req, error) {
                          // 如果收到錯誤嘗試重整(怕遞迴),不管他了,反正不是我要用
controller.reload();
},
onConsoleMessage: (controller, consoleMessage) {
print('Console message: ${consoleMessage.message}');
},
initialSettings: InAppWebViewSettings(
mixedContentMode:
MixedContentMode.MIXED_CONTENT_ALWAYS_ALLOW),
))));
}
}

因為沒有需要關閉app時disconnect vpn需求,我就暫時沒有研究這塊

但因為我是直接修改example/lib/main.dart檔案,所以沒辦法透過ide直接build apk那該怎麼做呢?答案是,直接透過terminal下指令

~% cd example

~% flutter build apk

這就做個紀念,提供給有需要的人,我要去補眠了



flutter第三彈,終究有寫不出來的時後,記錄連trojan-go vpn

先說本篇沒有任何程式碼,也不是教學文,畢竟最終結果是失敗的,只是一個紀錄

上一篇說到,公司提供trojan-go協議叫我試試看,我上網查一下除了我以前聽過的,近幾年又多出許多新協議,trojanp-go就是其中一個,他是走443port,偽裝https協議,是因為這樣所以C國不會擋?阿災,總之就動手做做看吧。想不到這一試,人生就浪費了近一週

首先估狗後只找到一個flutter plugin,最後一次commit是三年前.....能不擔心嗎==,然後整個plugin 還沒有上flutter hub,這令我有些擔心,把這個repo載下來後,嘗試放在資料夾內,試圖參照example資料夾的引用方式來因用這個plugin,但一直無法實際產生那些class,嘗試了好半天,第一步宣告放棄。

下一步詢問claude,他建議我到trojan官方github,把二進制的執行檔複製到手機裡,最後透過process.run去執行。原來是go寫的專案,那這一切都相當合理了,只是當一切看似進行順利的時候,最後卻出現Permission Denied......,無論怎麼修改執行權限都不通,最終一樣宣告放棄。

再次聯繫上水哥大神,用一塊雞排換得他的幫助,水哥先是到plugin的release把source code載下來,然整理到可以把example裡的專案run起來才轉給我,他說github上的code沒有aar檔,直接clone下來也是白搭,最後再說一句:這專案真的是爛透了。身為小白的我哪懂這些,只是再復述了一次:這專案真的是爛透了!

幾經折騰,確實把專案run起來了,但真的把配置送進去卻沒有真正連上vpn。

我左思右想,難道要在這裡放棄?既然這個plugin只是透過android跟aar溝通,那我是否可以直接寫android,行吧,在試最後一次

打開近十年沒有開啟的android專案,基本webview都建好了,然後我想說plugin的aar既然起碼是三年前的,那我去官方抓最新的來build好了,所以我重回官方github,然後再載gomobile來 build aar,然後一直都build不出來.....那能否直接使用plugin的aar?這部分我記憶有點模糊,但我印象好像跟新版的class function都不一樣,總之最終我還是放棄了

最終把這不幸的消息告知PM,卻換得一句:那我們再嘗試下一個協議吧!(靠X不是早說不行了嗎)......這趟旅程還要走多久呢==

2024年7月15日 星期一

flutter第二彈,公司要求要先連vpn才能進到我們站,先來試試open vpn吧

眾所周知,C國的網路環境一天天惡化,不想點辦法公司大家都快沒飯吃了。於是股東們提出個這麽天才的想法,如果連了VPN那不就可以繞過管制了嗎?

但骨子裡是前端的我,壓根不相信這事能成,不過轉念一想,既然市面上一堆軟體都可以做到軟體連線VPN,那這件事似乎沒有不可能,好吧那就試試看吧

對VPN協議我不太熟悉,索性選擇我最後一次使用的open vpn,我找到這裡有點資源可以借用,找到你喜歡的國家把ovpn載下來放到靜態資料夾裡

之後開始我們的程式

// add file vpn_webview.dart
import 'package:flutter/material.dart';
import 'package:openvpn_flutter/openvpn_flutter.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';

class VpnWebViewPage extends StatefulWidget {
const VpnWebViewPage({Key? key}) : super(key: key);

@override
_VpnWebViewPageState createState() => _VpnWebViewPageState();
}

class _VpnWebViewPageState extends State<VpnWebViewPage> {
late OpenVPN engine;
InAppWebViewController? webViewController;
final String webviewUrl = "https://xxxxxx.com/";
VpnStatus? status;
VPNStage? stage;
bool get isVpnConnected => status?.connectedOn != null;

@override
void initState() {
super.initState();
initPlatformState();
connectVpn();
}

Future<void> initPlatformState() async {
engine = OpenVPN(
onVpnStatusChanged: _onVpnStatusChanged,
onVpnStageChanged: _onVpnStageChanged,
);

engine.initialize(
groupIdentifier: "group.com.laskarmedia.vpnMobile",
providerBundleIdentifier: "id.laskarmedia.vpnMobileVpnExtension",
localizedDescription: "VPN by HD",
lastStage: (stage) {
print(stage.name);
},
lastStatus: (status) {
print(status);
},
);
}

void _onVpnStatusChanged(VpnStatus? _status) {
print('VPN Status: ${status.toString()}');
setState(() {
status = _status;
});
print('isVpnConnected: ${isVpnConnected}');
}

void _onVpnStageChanged(VPNStage? _stage, String string) {
setState(() {
stage = _stage;
});
if (string == VPNStage.disconnected) {
connectVpn();
}
}

Future<void> connectVpn() async {
// 替換為你的 VPN 配置文件路徑,記得去pubspec.yaml增加assets設定
String vpnConfig =
await DefaultAssetBundle.of(context).loadString('web/jac_test.ovpn');
engine.connect(vpnConfig, 'VPN Name');
}

@override
Widget build(BuildContext context) {
return Scaffold(
body: !isVpnConnected
? const Center(
child: CircularProgressIndicator(),
)
: InAppWebView(
initialUrlRequest: URLRequest(url: WebUri(webviewUrl)),
onWebViewCreated: (controller) {
webViewController = controller;
},
initialSettings: InAppWebViewSettings(
mixedContentMode:
MixedContentMode.MIXED_CONTENT_ALWAYS_ALLOW),
));

}
}

至於main那邊就只是引入使用,這邊就不演示了。

結果寫完了邏輯也沒啥問題,卻一直沒辦法成功。最終AI大神給予指示,何不試試清快取、模擬器重新啟動、重裝app試試,然後,然後就成功了....


7/15更新

結果維運說C國也封堵open vpn,建議我嘗試VPN的trojan-go協議....你倒是早說啊,還有你真當我純血android戰士嗎!

我上網爬了很久的文,幾乎沒有文獻說flutter可以做到,於是我拒絕了這項提案,

稍晚一點PM跑來敲我:ChatGPT說可以,你再試試吧!

王惹花小姐!你是不是沒有被AI唬過!當初想說只是幫點小忙,感覺越來越得寸進尺了!

HD鬼故事N+2集

今天故事比較短,但也是挺魔幻的

我們都知道祖國人沒有休假概念,所以假日來找你碴也是正常發揮,更何況前端又是個吃虧的單位,畢竟資料出不來,前端的錯!操作卡卡的,前端的錯!今天眼袋怎麼這麼腫,O的一定又是前端的錯!

事情發生在週日傍晚,一個本來好好的三方遊戲突然就會報錯(會說本來是因為工程師後來發現這段code已經有8個月沒動過了,至於8個月前是否是好的,那可能是另一個更魔幻的故事,你不要知道太多,對你人生比較好)

經過工程檢查後,因為api pending超過四秒連線被切斷了,在跟其他相同功能的專案交叉檢查,結果發現一個驚人的事實,兩個站打的api竟然不同支,這就很玄了喔!

於是工程師在群組裡回報了這件事,無論是api pending跟不同支api這兩件都需要後端協助查詢。

搞笑的來了,運營也不知是不是想幫後端說話,他說:想必是兩個站線路不同,因此需要打不同api吧!

半個多小時後,後端才慢慢打字回覆:我不確定

運營:....

(我默默覺得有人臉很腫)

然後......然後就隔天上班了!甚至都隔天下班了,這件事還沒有下文,我你個去!當前端好欺負是吧!!前端出事就要馬上處理,後端出事就可以讓人拖了又拖拖了又拖的嗎!保安!