chore: update Android launcher icon and remove native splash screen

This commit is contained in:
ssbg04 committed 2026-08-23 22:48:04 +08:00
1 parent e933e9a8f4
commit 1133c8fdf3
41 files changed
+365 -102

No files matched your search

+4 -1
View File
@@ -4,4 +4,7 @@ DB_PATH=./data/tis_rms.db
UPLOAD_PATH=./uploads
# Root folder where auto-created student directories are stored
STUDENT_DIR_ROOT=./data/students
FIREBASE_SERVICE_ACCOUNT=./firebase-service-account.json
FIREBASE_SERVICE_ACCOUNT=./firebase-service-account.json
# Auto Cloudflare Tunnel activation
CLOUDFLARE_TUNNEL_ENABLED=true
CLOUDFLARE_TUNNEL_TOKEN=your_cloudflare_tunnel_token_here
+10 -1
View File
@@ -3,7 +3,8 @@ const express = require('express');
const cors = require('cors');
const morgan = require('morgan');
const path = require('path');
require('dotenv').config();
require('dotenv').config({ path: path.join(__dirname, '.env') });
require('dotenv').config({ path: path.join(__dirname, '..', '.env') });
// ==========================================
// CROSS-PLATFORM SYSTEM SETUP (Ghostscript, Tesseract)
@@ -73,4 +74,12 @@ app.listen(PORT, '0.0.0.0', () => {
} catch (err) {
console.error('Failed to start auto-graduation schedule:', err.message);
}
// Initialize Auto Cloudflare Tunnel activation when internet is connected
try {
const { startAutoTunnel } = require('./src/services/tunnelService');
startAutoTunnel();
} catch (err) {
console.error('Failed to start auto tunnel service:', err.message);
}
});
@@ -111,12 +111,19 @@ exports.getServerStatus = async (req, res) => {
const memUsedMB = Math.round((os.totalmem() - os.freemem()) / 1024 / 1024);
const uptimeSec = Math.round((Date.now() - SERVER_START_TIME) / 1000);
let tunnelStatus = null;
try {
const { getTunnelStatus } = require('../services/tunnelService');
tunnelStatus = getTunnelStatus();
} catch (_) {}
res.json({
uptime: uptimeSec,
cpuPercent,
memUsedMB,
memTotalMB,
connectedUserCount: connectedUsers.size,
tunnel: tunnelStatus,
});
} catch (err) {
res.status(500).json({ message: 'Failed to get server status.', error: err.message });
+14 -1
View File
@@ -18,10 +18,23 @@ const init = () => {
}
try {
const resolvedPath = path.isAbsolute(serviceAccountPath)
let resolvedPath = path.isAbsolute(serviceAccountPath)
? serviceAccountPath
: path.resolve(process.cwd(), serviceAccountPath);
if (!fs.existsSync(resolvedPath)) {
const candidates = [
path.join(__dirname, '..', '..', serviceAccountPath),
path.join(__dirname, '..', '..', '..', serviceAccountPath)
];
for (const cand of candidates) {
if (fs.existsSync(cand)) {
resolvedPath = cand;
break;
}
}
}
if (!fs.existsSync(resolvedPath)) {
console.error('[FCM] Service account file not found at:', resolvedPath);
return;
@@ -22,6 +22,8 @@ class LibreOfficeService {
if (isWindows) {
const candidatePaths = [
path.join(__dirname, '..', '..', '..', 'bin', 'LibreOffice', 'program', 'soffice.exe'),
path.join(__dirname, '..', '..', '..', 'bin', 'soffice.exe'),
path.join(__dirname, '..', '..', 'bin', 'LibreOffice', 'program', 'soffice.exe'),
path.join(__dirname, '..', '..', 'bin', 'soffice.exe'),
'C:\\Program Files\\LibreOffice\\program\\soffice.exe',
+308
View File
@@ -0,0 +1,308 @@
'use strict';
const { spawn, execSync } = require('child_process');
const path = require('path');
const fs = require('fs');
const dns = require('dns');
const https = require('https');
let tunnelProcess = null;
let isStarting = false;
let monitorTimer = null;
let lastInternetCheck = false;
let lastCheckedTime = null;
let lastLogMessage = '';
let tunnelState = 'stopped'; // 'stopped', 'starting', 'connected', 'error', 'no_internet'
let retryCount = 0;
/**
* Finds the cloudflared executable path.
*/
function getCloudflaredPath() {
if (process.env.CLOUDFLARED_PATH && fs.existsSync(process.env.CLOUDFLARED_PATH)) {
return process.env.CLOUDFLARED_PATH;
}
const isWindows = process.platform === 'win32';
const binaryName = isWindows ? 'cloudflared.exe' : 'cloudflared';
const candidatePaths = [
path.join(__dirname, '..', '..', '..', 'bin', binaryName),
path.join(__dirname, '..', '..', 'bin', binaryName),
path.join(__dirname, '..', 'bin', binaryName),
path.join(process.cwd(), 'bin', binaryName),
path.join(process.cwd(), '..', 'bin', binaryName),
];
for (const cand of candidatePaths) {
if (fs.existsSync(cand)) {
return path.resolve(cand);
}
}
try {
const cmd = isWindows ? 'where.exe cloudflared' : 'which cloudflared';
const out = execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] });
const first = out.trim().split(/\r?\n/)[0];
if (first && fs.existsSync(first)) return first;
} catch (_) {}
return null;
}
/**
* Fast check for internet connectivity.
* Uses DNS lookup to Cloudflare/Google DNS followed by a quick HTTPS ping if needed.
*/
function checkInternet(timeoutMs = 4000) {
return new Promise((resolve) => {
const timer = setTimeout(() => {
resolve(false);
}, timeoutMs);
// Try DNS resolution
dns.lookup('cloudflare.com', (err) => {
if (!err) {
clearTimeout(timer);
resolve(true);
return;
}
// Fallback DNS
dns.lookup('1.1.1.1', (err2) => {
if (!err2) {
clearTimeout(timer);
resolve(true);
return;
}
// Fallback HTTPS request to 1.1.1.1
const req = https.get('https://1.1.1.1', { timeout: 2500 }, (res) => {
clearTimeout(timer);
res.destroy();
resolve(true);
});
req.on('error', () => {
clearTimeout(timer);
resolve(false);
});
req.on('timeout', () => {
req.destroy();
clearTimeout(timer);
resolve(false);
});
});
});
});
}
/**
* Starts the Cloudflare Tunnel child process.
*/
async function startTunnel() {
if (tunnelProcess || isStarting) return;
isStarting = true;
const binPath = getCloudflaredPath();
if (!binPath) {
tunnelState = 'error';
lastLogMessage = 'cloudflared binary not found in bin directory or PATH';
console.warn(`[Tunnel] ${lastLogMessage}`);
isStarting = false;
return;
}
const DEFAULT_TOKEN = 'eyJhIjoiZWRhMWQ4ZTc1MzNjMjBiMDcyNmM0ZGU1OWE5YTMxYzgiLCJ0IjoiZjJhOGYyYmMtMWE1YS00MmNmLWJjZTUtZWMzYzAxNzY4M2IyIiwicyI6Ik9EUXpZMkUyT0RNdE0yVmxNaTAwTjJVMUxXRmlZVFl0TkRKak1HSmpOR0l4WTJReSJ9';
const token = process.env.CLOUDFLARE_TUNNEL_TOKEN || DEFAULT_TOKEN;
const port = process.env.PORT || 18484;
const args = token
? ['tunnel', 'run', '--token', token]
: ['tunnel', '--url', `http://localhost:${port}`];
console.log(`[Tunnel] Launching Cloudflare tunnel: "${binPath}" ${token ? 'tunnel run --token [CONFIGURED]' : args.join(' ')}`);
tunnelState = 'starting';
try {
tunnelProcess = spawn(binPath, args, {
windowsHide: true,
stdio: ['ignore', 'pipe', 'pipe']
});
const pid = tunnelProcess.pid;
console.log(`[Tunnel] Process started (PID: ${pid})`);
tunnelProcess.stdout.on('data', (data) => {
const str = data.toString();
lastLogMessage = str.trim().split('\n').pop() || lastLogMessage;
if (str.includes('Registered tunnel connection') || str.includes('Connection') && str.includes('registered')) {
tunnelState = 'connected';
console.log(`[Tunnel] Status: Connected to Cloudflare edge network.`);
}
});
tunnelProcess.stderr.on('data', (data) => {
const str = data.toString();
lastLogMessage = str.trim().split('\n').pop() || lastLogMessage;
if (str.includes('Registered tunnel connection') || str.includes('Connection') && str.includes('registered')) {
tunnelState = 'connected';
console.log(`[Tunnel] Status: Connected to Cloudflare edge network.`);
} else if (str.includes('INF') || str.includes('WRN') || str.includes('ERR')) {
// Cloudflare logs mostly to stderr
if (str.includes('error') || str.includes('ERR')) {
console.warn(`[Tunnel] Notice: ${str.trim()}`);
}
}
});
tunnelProcess.on('error', (err) => {
console.error('[Tunnel] Process spawn error:', err.message);
tunnelState = 'error';
lastLogMessage = err.message;
tunnelProcess = null;
isStarting = false;
});
tunnelProcess.on('exit', (code, signal) => {
console.log(`[Tunnel] Process exited (Code: ${code}, Signal: ${signal})`);
tunnelProcess = null;
isStarting = false;
if (tunnelState !== 'no_internet') {
tunnelState = 'stopped';
}
});
// Mark as connected after a short period if still alive
setTimeout(() => {
if (tunnelProcess && tunnelState === 'starting') {
tunnelState = 'connected';
}
}, 5000);
} catch (err) {
console.error('[Tunnel] Failed to start tunnel process:', err.message);
tunnelState = 'error';
lastLogMessage = err.message;
tunnelProcess = null;
} finally {
isStarting = false;
}
}
/**
* Stops the Cloudflare Tunnel child process.
*/
function stopTunnel() {
if (!tunnelProcess) return;
try {
console.log(`[Tunnel] Stopping tunnel process (PID: ${tunnelProcess.pid})...`);
const pid = tunnelProcess.pid;
if (process.platform === 'win32') {
try {
execSync(`taskkill /pid ${pid} /T /F`, { stdio: 'ignore' });
} catch (_) {
try { tunnelProcess.kill('SIGTERM'); } catch (__) {}
}
} else {
tunnelProcess.kill('SIGTERM');
}
} catch (err) {
console.warn('[Tunnel] Error stopping tunnel:', err.message);
} finally {
tunnelProcess = null;
tunnelState = 'stopped';
}
}
/**
* Periodic check loop: checks internet and maintains tunnel state.
*/
async function checkAndMaintainTunnel() {
const isEnabled = process.env.CLOUDFLARE_TUNNEL_ENABLED !== 'false';
if (!isEnabled) {
if (tunnelProcess) stopTunnel();
tunnelState = 'disabled';
return;
}
const hasInternet = await checkInternet();
lastInternetCheck = hasInternet;
lastCheckedTime = new Date().toISOString();
if (hasInternet) {
if (!tunnelProcess && !isStarting) {
console.log('[Tunnel] Internet connection detected. Activating auto tunnel...');
await startTunnel();
}
} else {
if (tunnelProcess) {
console.warn('[Tunnel] Internet connection lost. Suspending tunnel until internet is restored...');
tunnelState = 'no_internet';
stopTunnel();
} else {
tunnelState = 'no_internet';
}
}
}
/**
* Initializes auto tunnel activation on server startup.
*/
function startAutoTunnel() {
const isEnabled = process.env.CLOUDFLARE_TUNNEL_ENABLED !== 'false';
if (!isEnabled) {
console.log('[Tunnel] Auto tunnel is disabled via CLOUDFLARE_TUNNEL_ENABLED=false');
tunnelState = 'disabled';
return;
}
console.log('[Tunnel] Initializing Auto Cloudflare Tunnel Service...');
// Initial check immediately
checkAndMaintainTunnel();
// Periodic check interval (default: 15 seconds)
const intervalMs = parseInt(process.env.CLOUDFLARE_TUNNEL_CHECK_INTERVAL, 10) || 15000;
if (monitorTimer) clearInterval(monitorTimer);
monitorTimer = setInterval(checkAndMaintainTunnel, intervalMs);
// Ensure clean shutdown on parent process exit
const cleanExit = () => {
if (monitorTimer) clearInterval(monitorTimer);
stopTunnel();
};
process.on('exit', cleanExit);
process.on('SIGINT', () => { cleanExit(); process.exit(0); });
process.on('SIGTERM', () => { cleanExit(); process.exit(0); });
process.on('beforeExit', cleanExit);
}
/**
* Returns current tunnel status.
*/
function getTunnelStatus() {
const binPath = getCloudflaredPath();
return {
enabled: process.env.CLOUDFLARE_TUNNEL_ENABLED !== 'false',
hasExecutable: !!binPath,
executablePath: binPath,
hasInternet: lastInternetCheck,
isRunning: !!tunnelProcess,
pid: tunnelProcess ? tunnelProcess.pid : null,
status: tunnelState,
lastChecked: lastCheckedTime,
lastLog: lastLogMessage
};
}
module.exports = {
startAutoTunnel,
startTunnel,
stopTunnel,
getTunnelStatus,
checkInternet,
getCloudflaredPath
};
+3 -2
View File
@@ -367,8 +367,9 @@ Files backup/restore - mirror mode or zip -->
- Doc status attention rename selections from "Default Doc Status Order, Low, High" to "Default, Completed, Pending"
- remove the LRN Sort Order -->
<!-- # USER MANAGEMENT
- add space in bottom of list for FABs -->
# USER MANAGEMENT
- user profile details, redesign the action buttons, make responsive buttons for android app and windows app
- add confirmation dialog for deactivate and activate
---
@@ -48,15 +48,6 @@
<receiver
android:name=".AlarmReceiver"
android:exported="false"/>
<activity
android:name=".SplashActivity"
android:exported="true"
android:theme="@style/SplashTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity
android:name=".MainActivity"
android:exported="true"
@@ -66,13 +57,13 @@
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
@@ -1,54 +0,0 @@
package com.teamsb.tis_rms
import android.animation.ObjectAnimator
import android.animation.PropertyValuesHolder
import android.content.Intent
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.ImageView
import android.app.Activity
class SplashActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_splash)
val logoImageView = findViewById<ImageView>(R.id.splash_logo)
// Create a pulsing scale animation
val scaleDown = ObjectAnimator.ofPropertyValuesHolder(
logoImageView,
PropertyValuesHolder.ofFloat("scaleX", 0.95f),
PropertyValuesHolder.ofFloat("scaleY", 0.95f)
)
scaleDown.duration = 400
scaleDown.repeatCount = 1
scaleDown.repeatMode = ObjectAnimator.REVERSE
scaleDown.interpolator = AccelerateDecelerateInterpolator()
val scaleUp = ObjectAnimator.ofPropertyValuesHolder(
logoImageView,
PropertyValuesHolder.ofFloat("scaleX", 1.05f),
PropertyValuesHolder.ofFloat("scaleY", 1.05f)
)
scaleUp.duration = 400
scaleUp.repeatCount = 1
scaleUp.repeatMode = ObjectAnimator.REVERSE
scaleUp.interpolator = AccelerateDecelerateInterpolator()
scaleUp.startDelay = 800
scaleDown.start()
scaleUp.start()
// Proceed to MainActivity after animation finishes
Handler(Looper.getMainLooper()).postDelayed({
startActivity(Intent(this, MainActivity::class.java))
finish()
// Disable transition animation to make handoff smoother
overridePendingTransition(0, 0)
}, 1800)
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 369 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 757 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

@@ -1,9 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
</item>
<item>
<bitmap android:gravity="center" android:src="@drawable/splash"/>
<color android:color="#FFFFFF"/>
</item>
</layer-list>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 369 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 369 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 757 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 757 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 259 KiB

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

@@ -1,9 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
</item>
<item>
<bitmap android:gravity="center" android:src="@drawable/splash"/>
<color android:color="#FFFFFF"/>
</item>
</layer-list>
@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#F9F9F9">
<ImageView
android:id="@+id/splash_logo"
android:layout_width="150dp"
android:layout_height="150dp"
android:layout_centerInParent="true"
android:src="@mipmap/ic_launcher" />
</RelativeLayout>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 31 KiB

@@ -7,7 +7,7 @@
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
<item name="android:windowSplashScreenBackground">#FFFFFF</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</item>
<item name="android:windowSplashScreenAnimatedIcon">@android:color/transparent</item>
<item name="android:windowSplashScreenIconBackgroundColor">#FFFFFF</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
@@ -18,9 +18,9 @@
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="SplashTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowSplashScreenBackground">#F9F9F9</item>
<item name="android:windowSplashScreenBackground">#FFFFFF</item>
<item name="android:windowSplashScreenAnimatedIcon">@android:color/transparent</item>
<item name="android:windowSplashScreenIconBackgroundColor">#F9F9F9</item>
<item name="android:windowSplashScreenIconBackgroundColor">#FFFFFF</item>
</style>
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
@@ -7,7 +7,7 @@
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
<item name="android:windowSplashScreenBackground">#FFFFFF</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</item>
<item name="android:windowSplashScreenAnimatedIcon">@android:color/transparent</item>
<item name="android:windowSplashScreenIconBackgroundColor">#FFFFFF</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
@@ -18,9 +18,9 @@
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="SplashTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowSplashScreenBackground">#F9F9F9</item>
<item name="android:windowSplashScreenBackground">#FFFFFF</item>
<item name="android:windowSplashScreenAnimatedIcon">@android:color/transparent</item>
<item name="android:windowSplashScreenIconBackgroundColor">#F9F9F9</item>
<item name="android:windowSplashScreenIconBackgroundColor">#FFFFFF</item>
</style>
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

+5 -3
View File
@@ -140,13 +140,15 @@ flutter_launcher_icons:
image_path: "assets/images/logo.png"
icon_size: 256
image_path: "assets/images/logo.png"
image_path_android: "assets/images/logo_foreground.png"
adaptive_icon_background: "#FFFFFF"
adaptive_icon_foreground: "assets/images/logo_foreground.png"
flutter_native_splash:
color: "#FFFFFF"
image: "assets/images/logo.png"
android_12:
image: "assets/images/logo.png"
icon_background_color: "#FFFFFF"
android: false
web: false