Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
56 changes: 56 additions & 0 deletions .github/workflows/release-android.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: Release Android

on:
workflow_dispatch:

permissions:
contents: write

jobs:
build-android:
name: Build Android APK
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Set up Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'

- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
cache: true

- name: Install dependencies
run: flutter pub get

- name: Build release APK
run: flutter build apk --release

- name: Prepare APK artifact
shell: bash
run: |
mkdir -p release-assets
cp build/app/outputs/flutter-apk/app-release.apk release-assets/AndroidIRCx-Flutter-android.apk

- name: Upload workflow artifact
uses: actions/upload-artifact@v4
with:
name: android-release-apk
path: release-assets/AndroidIRCx-Flutter-android.apk

- name: Attach APK to GitHub release
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}
generate_release_notes: true
files: release-assets/AndroidIRCx-Flutter-android.apk
59 changes: 59 additions & 0 deletions .github/workflows/release-windows.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: Release Windows

on:
workflow_dispatch:

permissions:
contents: write

jobs:
build-windows:
name: Build Windows Package
runs-on: windows-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
cache: true

- name: Install dependencies
run: flutter pub get

- name: Enable Windows desktop
run: flutter config --enable-windows-desktop

- name: Build Windows release
run: flutter build windows --release

- name: Package Windows release
shell: pwsh
run: |
$artifactDir = "release-assets"
New-Item -ItemType Directory -Force -Path $artifactDir | Out-Null
$source = "build/windows/x64/runner/Release"
$zipPath = Join-Path $artifactDir "AndroidIRCx-Flutter-windows.zip"
if (Test-Path $zipPath) {
Remove-Item $zipPath -Force
}
Compress-Archive -Path "$source/*" -DestinationPath $zipPath

- name: Upload workflow artifact
uses: actions/upload-artifact@v4
with:
name: windows-release-zip
path: release-assets/AndroidIRCx-Flutter-windows.zip

- name: Attach Windows package to GitHub release
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}
generate_release_notes: true
files: release-assets/AndroidIRCx-Flutter-windows.zip
4 changes: 4 additions & 0 deletions devtools_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:
- shared_preferences: true
22 changes: 22 additions & 0 deletions lib/core/models/app_settings.dart
Original file line number Diff line number Diff line change
@@ -1,27 +1,49 @@
enum NoticeRoutingMode { server, active, notice, private }

class AppSettings {
const AppSettings({
this.showRawEvents = true,
this.noticeRouting = NoticeRoutingMode.server,
this.showHeaderSearchButton = true,
this.showAttachmentPreviews = true,
});

final bool showRawEvents;
final NoticeRoutingMode noticeRouting;
final bool showHeaderSearchButton;
final bool showAttachmentPreviews;

AppSettings copyWith({
bool? showRawEvents,
NoticeRoutingMode? noticeRouting,
bool? showHeaderSearchButton,
bool? showAttachmentPreviews,
}) {
return AppSettings(
showRawEvents: showRawEvents ?? this.showRawEvents,
noticeRouting: noticeRouting ?? this.noticeRouting,
showHeaderSearchButton: showHeaderSearchButton ?? this.showHeaderSearchButton,
showAttachmentPreviews: showAttachmentPreviews ?? this.showAttachmentPreviews,
);
}

Map<String, Object?> toJson() {
return {
'showRawEvents': showRawEvents,
'noticeRouting': noticeRouting.name,
'showHeaderSearchButton': showHeaderSearchButton,
'showAttachmentPreviews': showAttachmentPreviews,
};
}

factory AppSettings.fromJson(Map<String, Object?> json) {
return AppSettings(
showRawEvents: (json['showRawEvents'] as bool?) ?? true,
noticeRouting: json['noticeRouting'] is String
? NoticeRoutingMode.values.byName(json['noticeRouting']! as String)
: NoticeRoutingMode.server,
showHeaderSearchButton: (json['showHeaderSearchButton'] as bool?) ?? true,
showAttachmentPreviews: (json['showAttachmentPreviews'] as bool?) ?? true,
);
}
}
2 changes: 1 addition & 1 deletion lib/core/models/chat_tab.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
enum ChatTabType { server, channel, query, notice }
enum ChatTabType { server, channel, query, notice, dcc }

class ChatTab {
const ChatTab({
Expand Down
71 changes: 71 additions & 0 deletions lib/core/models/dcc_session.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
enum DccSessionType { chat, send, unknown }

enum DccSessionStatus { pending, offering, connecting, connected, closed, failed }

class DccSession {
const DccSession({
required this.id,
required this.tabId,
required this.peerNick,
required this.type,
required this.status,
required this.direction,
this.filename,
this.host,
this.port,
this.size,
this.token,
this.filePath,
this.bytesTransferred = 0,
this.error,
});

final String id;
final String tabId;
final String peerNick;
final DccSessionType type;
final DccSessionStatus status;
final String direction;
final String? filename;
final String? host;
final int? port;
final int? size;
final String? token;
final String? filePath;
final int bytesTransferred;
final String? error;

DccSession copyWith({
String? id,
String? tabId,
String? peerNick,
DccSessionType? type,
DccSessionStatus? status,
String? direction,
String? filename,
String? host,
int? port,
int? size,
String? token,
String? filePath,
int? bytesTransferred,
String? error,
}) {
return DccSession(
id: id ?? this.id,
tabId: tabId ?? this.tabId,
peerNick: peerNick ?? this.peerNick,
type: type ?? this.type,
status: status ?? this.status,
direction: direction ?? this.direction,
filename: filename ?? this.filename,
host: host ?? this.host,
port: port ?? this.port,
size: size ?? this.size,
token: token ?? this.token,
filePath: filePath ?? this.filePath,
bytesTransferred: bytesTransferred ?? this.bytesTransferred,
error: error ?? this.error,
);
}
}
8 changes: 8 additions & 0 deletions lib/core/models/irc_message.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ class IrcMessage {
required this.sender,
required this.content,
required this.timestamp,
this.tags = const <String, String?>{},
this.isPlayback = false,
this.isOwn = false,
this.kind = IrcMessageKind.chat,
});
Expand All @@ -16,6 +18,8 @@ class IrcMessage {
final String sender;
final String content;
final DateTime timestamp;
final Map<String, String?> tags;
final bool isPlayback;
final bool isOwn;
final IrcMessageKind kind;

Expand All @@ -26,6 +30,8 @@ class IrcMessage {
'sender': sender,
'content': content,
'timestamp': timestamp.toIso8601String(),
'tags': tags,
'isPlayback': isPlayback,
'isOwn': isOwn,
'kind': kind.name,
};
Expand All @@ -38,6 +44,8 @@ class IrcMessage {
sender: json['sender']! as String,
content: json['content']! as String,
timestamp: DateTime.parse(json['timestamp']! as String),
tags: Map<String, String?>.from((json['tags'] as Map?) ?? const <String, String?>{}),
isPlayback: (json['isPlayback'] as bool?) ?? false,
isOwn: (json['isOwn'] as bool?) ?? false,
kind: IrcMessageKind.values.byName(
(json['kind'] as String?) ?? IrcMessageKind.chat.name,
Expand Down
21 changes: 21 additions & 0 deletions lib/dcc/services/dcc_file_store.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import 'dcc_file_store_stub.dart' if (dart.library.io) 'dcc_file_store_io.dart';

abstract class DccFileSink {
void add(List<int> bytes);

Future<void> flush();

Future<void> close();
}

typedef DccTempFile = ({String path, DccFileSink sink});
typedef DccSourceFile = ({
String path,
String fileName,
int size,
Future<List<int>> Function() readAllBytes,
});

Future<DccTempFile> createDccTempFile(String fileName) => createPlatformDccTempFile(fileName);

Future<DccSourceFile> openDccSourceFile(String path) => openPlatformDccSourceFile(path);
44 changes: 44 additions & 0 deletions lib/dcc/services/dcc_file_store_io.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import 'dart:io';

import 'dcc_file_store.dart';

class _IoDccFileSink implements DccFileSink {
_IoDccFileSink(this._sink);

final IOSink _sink;

@override
void add(List<int> bytes) {
_sink.add(bytes);
}

@override
Future<void> close() => _sink.close();

@override
Future<void> flush() => _sink.flush();
}

Future<DccTempFile> createPlatformDccTempFile(String fileName) async {
final sanitized = fileName.replaceAll(RegExp(r'[\\/:*?"<>|]'), '_');
final path = '${Directory.systemTemp.path}/$sanitized';
final file = File(path);
final sink = file.openWrite(mode: FileMode.writeOnly);
return (path: path, sink: _IoDccFileSink(sink));
}

Future<DccSourceFile> openPlatformDccSourceFile(String path) async {
final file = File(path);
final exists = await file.exists();
if (!exists) {
throw FileSystemException('DCC source file does not exist.', path);
}

final stat = await file.stat();
return (
path: file.path,
fileName: file.uri.pathSegments.isEmpty ? file.path : file.uri.pathSegments.last,
size: stat.size,
readAllBytes: file.readAsBytes,
);
}
9 changes: 9 additions & 0 deletions lib/dcc/services/dcc_file_store_stub.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import 'dcc_file_store.dart';

Future<DccTempFile> createPlatformDccTempFile(String fileName) async {
throw UnsupportedError('DCC file storage is only supported on IO platforms.');
}

Future<DccSourceFile> openPlatformDccSourceFile(String path) async {
throw UnsupportedError('Outgoing DCC SEND is only supported on IO platforms.');
}
Loading
Loading